Skip to content

Commit 1793dd4

Browse files
authored
Merge pull request #327 from ProjectMapK/develop
Release 2025-05-04 14:24:08 +0000
2 parents 042852a + 91e5bb0 commit 1793dd4

16 files changed

Lines changed: 401 additions & 71 deletions

File tree

.github/workflows/release.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
name: release
22
on:
33
workflow_dispatch:
4+
5+
permissions:
6+
pull-requests: write
7+
48
jobs:
59
gitPrRelease:
610
name: git-pr-release

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ val jacksonVersion = libs.versions.jackson.get()
1616
val generatedSrcPath = "${layout.buildDirectory.get()}/generated/kotlin"
1717

1818
group = groupStr
19-
version = "${jacksonVersion}-beta20"
19+
version = "${jacksonVersion}-beta21"
2020

2121
repositories {
2222
mavenCentral()

gradle/libs.versions.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[versions]
22
kotlin = "1.9.25" # Mainly for CI, it can be rewritten by environment variable.
3-
jackson = "2.18.3"
3+
jackson = "2.19.0"
44

55
# test libs
66
junit = "5.12.2"
@@ -14,10 +14,10 @@ jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations
1414
# test libs
1515
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" }
1616
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
17-
mockk = "io.mockk:mockk:1.14.0"
17+
mockk = "io.mockk:mockk:1.14.2"
1818
jackson-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml", version.ref = "jackson" }
1919
jackson-csv = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-csv", version.ref = "jackson" }
2020
jackson-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
2121

2222
[plugins]
23-
kotlinter = { id = "org.jmailen.kotlinter", version = "5.0.1" }
23+
kotlinter = { id = "org.jmailen.kotlinter", version = "5.0.2" }

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Extensions.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,8 @@ public fun ObjectMapper.registerKotlinModule(
5555

5656
public inline fun <reified T> jacksonTypeRef(): TypeReference<T> = object : TypeReference<T>() {}
5757

58-
/**
59-
* It is public due to Kotlin restrictions, but should not be used externally.
60-
*/
61-
public inline fun <reified T> Any?.checkTypeMismatch(): T {
58+
@PublishedApi
59+
internal inline fun <reified T> Any?.checkTypeMismatch(): T {
6260
// Basically, this check assumes that T is non-null and the value is null.
6361
// Since this can be caused by both input or ObjectMapper implementation errors,
6462
// a more abstract RuntimeJsonMappingException is thrown.
@@ -84,10 +82,19 @@ public inline fun <reified T> Any?.checkTypeMismatch(): T {
8482
public inline fun <reified T> ObjectMapper.readValue(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
8583
.checkTypeMismatch()
8684

87-
// TODO: After importing 2.19, import the changes in kotlin-module and uncomment the tests.
88-
public inline fun <reified T> ObjectMapper.readValues(
89-
jp: JsonParser
90-
): MappingIterator<T> = readValues(jp, jacksonTypeRef<T>())
85+
/**
86+
* Shorthand for [ObjectMapper.readValues].
87+
* @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
88+
* Other cases where the read value is of a different type than [T]
89+
* due to an incorrect customization to [ObjectMapper].
90+
*/
91+
public inline fun <reified T> ObjectMapper.readValues(jp: JsonParser): MappingIterator<T> {
92+
val values = readValues(jp, jacksonTypeRef<T>())
93+
94+
return object : MappingIterator<T>(values) {
95+
override fun nextValue(): T = super.nextValue().checkTypeMismatch()
96+
}
97+
}
9198

9299
/**
93100
* Shorthand for [ObjectMapper.readValue].

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinPrimaryAnnotationIntrospector.kt

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.github.projectmapk.jackson.module.kogera.annotationIntrospector
22

3+
import com.fasterxml.jackson.annotation.JsonProperty
4+
import com.fasterxml.jackson.annotation.OptBoolean
35
import com.fasterxml.jackson.databind.JavaType
46
import com.fasterxml.jackson.databind.introspect.Annotated
57
import com.fasterxml.jackson.databind.introspect.AnnotatedField
@@ -26,29 +28,37 @@ internal class KotlinPrimaryAnnotationIntrospector(
2628
private val nullToEmptyMap: Boolean,
2729
private val cache: ReflectionCache
2830
) : NopAnnotationIntrospector() {
31+
// If a new isRequired is explicitly specified or the old required is true, those values take precedence.
32+
// In other cases, override is done by KotlinModule.
33+
private fun JsonProperty.forceRequiredByAnnotation(): Boolean? = when {
34+
isRequired != OptBoolean.DEFAULT -> isRequired.asBoolean()
35+
required -> true
36+
else -> null
37+
}
38+
2939
// If JsonProperty.required is true, the behavior is clearly specified and the result is paramount.
3040
// Otherwise, the required is determined from the configuration and the definition on Kotlin.
3141
override fun hasRequiredMarker(m: AnnotatedMember): Boolean? {
32-
val byAnnotation = _findAnnotation(m, JSON_PROPERTY_CLASS)
33-
?.let { if (it.required) return true else false }
42+
return cache.getJmClass(m.member.declaringClass)?.let { jmClass ->
43+
// To avoid overwriting processing by other modules, annotations are checked after JmClass has been obtained
44+
_findAnnotation(m, JSON_PROPERTY_CLASS)
45+
?.forceRequiredByAnnotation()
46+
?.let { return it }
3447

35-
return cache.getJmClass(m.member.declaringClass)?.let {
3648
when (m) {
37-
is AnnotatedField -> m.hasRequiredMarker(it)
38-
is AnnotatedMethod -> m.getRequiredMarkerFromCorrespondingAccessor(it)
39-
is AnnotatedParameter -> m.hasRequiredMarker(it)
49+
is AnnotatedField -> m.hasRequiredMarker(jmClass)
50+
is AnnotatedMethod -> m.getRequiredMarkerFromCorrespondingAccessor(jmClass)
51+
is AnnotatedParameter -> m.hasRequiredMarker(jmClass)
4052
else -> null
4153
}
42-
} ?: byAnnotation // If a JsonProperty is available, use it to reduce processing costs.
54+
}
4355
}
4456

4557
// Functions that call this may return incorrect results for value classes whose value type is Collection or Map,
4658
// but this is a rare case and difficult to handle, so it is not supported.
4759
private fun JavaType.hasDefaultEmptyValue() = (nullToEmptyCollection && isCollectionLikeType) ||
4860
(nullToEmptyMap && isMapLikeType)
4961

50-
// The nullToEmpty option also affects serialization,
51-
// but deserialization is preferred because there is currently no way to distinguish between contexts.
5262
private fun AnnotatedField.hasRequiredMarker(jmClass: JmClass): Boolean? {
5363
// Direct access to `AnnotatedField` is only performed if there is no accessor (defined as JvmField),
5464
// so if an accessor is defined, it is ignored.
@@ -57,7 +67,7 @@ internal class KotlinPrimaryAnnotationIntrospector(
5767
// only a check for the existence of a getter is performed.
5868
// https://youtrack.jetbrains.com/issue/KT-6519
5969
?.let {
60-
if (it.getterName == null) !(it.returnType.isNullable || type.hasDefaultEmptyValue()) else null
70+
if (it.getterName == null) !it.returnType.isNullable else null
6171
}
6272
}
6373

@@ -68,12 +78,8 @@ internal class KotlinPrimaryAnnotationIntrospector(
6878
): Boolean? = when (parameterCount) {
6979
0 -> jmClass.findPropertyByGetter(member)?.isRequiredByNullability()
7080
1 -> {
71-
if (this.getParameter(0).type.hasDefaultEmptyValue()) {
72-
false
73-
} else {
74-
val memberSignature = member.toSignature()
75-
jmClass.properties.find { it.setterSignature == memberSignature }?.isRequiredByNullability()
76-
}
81+
val memberSignature = member.toSignature()
82+
jmClass.properties.find { it.setterSignature == memberSignature }?.isRequiredByNullability()
7783
}
7884
else -> null
7985
}

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ import org.junit.jupiter.api.Nested
1111
import org.junit.jupiter.api.Test
1212

1313
private class ArgumentBucketTest {
14-
val mockConverters: List<ValueClassUnboxConverter<Any>?> = mockk {
15-
every { this@mockk[any()] } returns null
16-
}
17-
1814
fun mockValueParameter(mockVararg: Boolean = false, mockOptional: Boolean = false) = mockk<JmValueParameter> {
1915
every { isVararg } returns mockVararg
2016
every { isOptional } returns mockOptional
@@ -27,7 +23,7 @@ private class ArgumentBucketTest {
2723
val generator = BucketGenerator(
2824
(0..31).map { String::class.java },
2925
(0..31).map { mockValueParameter() },
30-
mockConverters
26+
emptyList()
3127
)
3228
val result = generator.generate()
3329

@@ -51,7 +47,7 @@ private class ArgumentBucketTest {
5147
Double::class.java
5248
),
5349
(1..8).map { mockValueParameter() },
54-
mockConverters
50+
emptyList()
5551
)
5652
val result = generator.generate()
5753

@@ -70,7 +66,7 @@ private class ArgumentBucketTest {
7066
val generator = BucketGenerator(
7167
(0..32).map { String::class.java },
7268
(0..32).map { mockValueParameter() },
73-
mockConverters
69+
emptyList()
7470
)
7571
val result = generator.generate()
7672

@@ -89,7 +85,7 @@ private class ArgumentBucketTest {
8985
val generator = BucketGenerator(
9086
(0..32).map { String::class.java },
9187
(0..32).map { mockValueParameter() },
92-
mockConverters
88+
List(33) { null }
9389
)
9490
val sut = generator.generate()
9591

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/HasRequiredMarkerTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ class HasRequiredMarkerTest {
108108

109109
assertFalse(desc.isRequired("nullableProp"))
110110
assertFalse(desc.isRequired("nullableField"))
111-
assertFalse(desc.isRequired("collectionProp"))
112-
assertFalse(desc.isRequired("collectionField"))
113-
assertFalse(desc.isRequired("mapProp"))
114-
assertFalse(desc.isRequired("mapField"))
111+
assertTrue(desc.isRequired("collectionProp"))
112+
assertTrue(desc.isRequired("collectionField"))
113+
assertTrue(desc.isRequired("mapProp"))
114+
assertTrue(desc.isRequired("mapField"))
115115
assertTrue(desc.isRequired("nonNullProp"))
116116
assertTrue(desc.isRequired("nonNullField"))
117117
}

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/HasRequiredMarkerTest.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,8 @@ class HasRequiredMarkerTest {
6767
val map: Map<*, *> = emptyMap<Any, Any>()
6868
}
6969

70-
// @see KotlinPrimaryAnnotationIntrospector::AnnotatedField.hasRequiredMarker
7170
@Test
72-
fun failing() {
71+
fun `nullToEmpty does not affect for field`() {
7372
val nullToDefaultMapper = ObjectMapper().registerModule(
7473
KotlinModule.Builder()
7574
.enable(KotlinFeature.NullToEmptyCollection)
@@ -78,13 +77,7 @@ class HasRequiredMarkerTest {
7877
)
7978
val desc = nullToDefaultMapper.introspectSer<NullToDefaultTarget>()
8079

81-
assertFalse(
82-
desc.isRequired("collection"),
83-
"KotlinPrimaryAnnotationIntrospector::AnnotatedField.hasRequiredMarker fixed"
84-
)
85-
assertFalse(
86-
desc.isRequired("map"),
87-
"KotlinPrimaryAnnotationIntrospector::AnnotatedField.hasRequiredMarker fixed"
88-
)
80+
assertTrue(desc.isRequired("collection"))
81+
assertTrue(desc.isRequired("map"))
8982
}
9083
}

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/ReadValueTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ReadValueTest {
2121
val src = defaultMapper.createParser("null")
2222
assertThrows<RuntimeJsonMappingException> {
2323
defaultMapper.readValue<String>(src)
24-
}.printStackTrace()
24+
}
2525
}
2626

2727
@Test

src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zPorted/ReadValuesTest.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ class ReadValuesTest {
3838
val itr = mapper.readValues<String>(src)
3939

4040
assertEquals("foo", itr.next())
41-
// TODO: It is expected to be checked after importing 2.19.
42-
// assertThrows<RuntimeJsonMappingException> {
43-
assertDoesNotThrow {
41+
assertThrows<RuntimeJsonMappingException> {
4442
itr.next()
4543
}
4644
}
@@ -51,9 +49,7 @@ class ReadValuesTest {
5149
val itr = mapper.readValues<String>(src)
5250

5351
assertEquals("foo", itr.nextValue())
54-
// TODO: It is expected to be checked after importing 2.19.
55-
// assertThrows<RuntimeJsonMappingException> {
56-
assertDoesNotThrow {
52+
assertThrows<RuntimeJsonMappingException> {
5753
itr.nextValue()
5854
}
5955
}

0 commit comments

Comments
 (0)