Skip to content

Commit bd35bb8

Browse files
authored
Merge pull request #244 from ProjectMapK/develop
Release 2024-11-23 16:34:29 +0000
2 parents 2015dfa + 4409175 commit bd35bb8

File tree

10 files changed

+127
-87
lines changed

10 files changed

+127
-87
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ This project makes several disruptive changes to achieve more `Kotlin-like` beha
3030
Details are summarized in [KogeraSpecificImplementations](./docs/KogeraSpecificImplementations.md).
3131

3232
# Compatibility
33-
- `jackson 2.17.x`
33+
- `jackson 2.18.x`
3434
- `Java 8+`
3535
- `Kotlin 1.8.22+`
3636

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}-beta14"
19+
version = "${jacksonVersion}-beta15"
2020

2121
repositories {
2222
mavenCentral()

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[versions]
22
kotlin = "1.8.22" # Mainly for CI, it can be rewritten by environment variable.
3-
jackson = "2.17.3"
3+
jackson = "2.18.1"
44

55
# test libs
66
junit = "5.11.3"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public inline fun <reified T> ObjectMapper.readValue(src: ByteArray): T = readVa
6262

6363
public inline fun <reified T> ObjectMapper.treeToValue(n: TreeNode): T =
6464
readValue(this.treeAsTokens(n), jacksonTypeRef<T>())
65-
public inline fun <reified T> ObjectMapper.convertValue(from: Any): T = convertValue(from, jacksonTypeRef<T>())
65+
public inline fun <reified T> ObjectMapper.convertValue(from: Any?): T = convertValue(from, jacksonTypeRef<T>())
6666

6767
public inline fun <reified T> ObjectReader.readValueTyped(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
6868
public inline fun <reified T> ObjectReader.readValuesTyped(jp: JsonParser): Iterator<T> =

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

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

3+
import com.fasterxml.jackson.annotation.JsonProperty
34
import com.fasterxml.jackson.annotation.JsonSetter
45
import com.fasterxml.jackson.annotation.Nulls
56
import com.fasterxml.jackson.databind.JavaType
@@ -11,11 +12,14 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedMember
1112
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
1213
import com.fasterxml.jackson.databind.introspect.AnnotatedParameter
1314
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
15+
import com.fasterxml.jackson.databind.introspect.PotentialCreator
1416
import com.fasterxml.jackson.databind.util.Converter
1517
import io.github.projectmapk.jackson.module.kogera.JSON_K_UNBOX_CLASS
1618
import io.github.projectmapk.jackson.module.kogera.KOTLIN_DURATION_CLASS
1719
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
1820
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
21+
import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass
22+
import io.github.projectmapk.jackson.module.kogera.jmClass.JmConstructor
1923
import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter
2024
import io.github.projectmapk.jackson.module.kogera.ser.KotlinDurationValueToJavaDurationConverter
2125
import io.github.projectmapk.jackson.module.kogera.ser.KotlinToJavaDurationConverter
@@ -120,13 +124,57 @@ internal class KotlinFallbackAnnotationIntrospector(
120124
}
121125
}
122126
?: super.findSetterInfo(ann)
127+
128+
// If it is not a Kotlin class or an Enum, Creator is not used
129+
private fun AnnotatedClass.creatableKotlinClass(): JmClass? = annotated
130+
.takeIf { !it.isEnum }
131+
?.let { cache.getJmClass(it) }
132+
133+
override fun findDefaultCreator(
134+
config: MapperConfig<*>,
135+
valueClass: AnnotatedClass,
136+
declaredConstructors: List<PotentialCreator>,
137+
declaredFactories: List<PotentialCreator>
138+
): PotentialCreator? {
139+
val jmClass = valueClass.creatableKotlinClass() ?: return null
140+
val primarilyConstructor = jmClass.primarilyConstructor()
141+
?.takeIf { it.valueParameters.isNotEmpty() }
142+
?: return null
143+
val isPossiblySingleString = isPossiblySingleString(primarilyConstructor, jmClass)
144+
145+
for (it in declaredConstructors) {
146+
val javaConstructor = it.creator().annotated as Constructor<*>
147+
148+
if (primarilyConstructor.isMetadataFor(javaConstructor)) {
149+
if (isPossibleSingleString(isPossiblySingleString, javaConstructor)) {
150+
break
151+
} else {
152+
return it
153+
}
154+
}
155+
}
156+
157+
return null
158+
}
123159
}
124160

125161
private fun JmValueParameter.isNullishTypeAt(index: Int): Boolean = arguments.getOrNull(index)?.let {
126162
// If it is not a StarProjection, type is not null
127163
it === KmTypeProjection.STAR || it.type!!.isNullable
128-
} ?: true // If a type argument cannot be taken, treat it as nullable to avoid unexpected failure.
164+
} != false // If a type argument cannot be taken, treat it as nullable to avoid unexpected failure.
129165

130166
private fun JmValueParameter.requireStrictNullCheck(type: JavaType): Boolean =
131167
((type.isArrayType || type.isCollectionLikeType) && !this.isNullishTypeAt(0)) ||
132168
(type.isMapLikeType && !this.isNullishTypeAt(1))
169+
170+
private fun JmClass.primarilyConstructor() = constructors.find { !it.isSecondary } ?: constructors.singleOrNull()
171+
172+
private fun isPossiblySingleString(
173+
jmConstructor: JmConstructor,
174+
jmClass: JmClass
175+
) = jmConstructor.valueParameters.singleOrNull()?.let { it.isString && it.name !in jmClass.propertyNameSet } == true
176+
177+
private fun isPossibleSingleString(
178+
isPossiblySingleString: Boolean,
179+
javaConstructor: Constructor<*>
180+
): Boolean = isPossiblySingleString && javaConstructor.parameters[0].annotations.none { it is JsonProperty }

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

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

3-
import com.fasterxml.jackson.annotation.JsonCreator
4-
import com.fasterxml.jackson.annotation.JsonProperty
53
import com.fasterxml.jackson.databind.JavaType
6-
import com.fasterxml.jackson.databind.cfg.MapperConfig
74
import com.fasterxml.jackson.databind.introspect.Annotated
8-
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor
95
import com.fasterxml.jackson.databind.introspect.AnnotatedField
106
import com.fasterxml.jackson.databind.introspect.AnnotatedMember
117
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
@@ -14,18 +10,13 @@ import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
1410
import com.fasterxml.jackson.databind.jsontype.NamedType
1511
import io.github.projectmapk.jackson.module.kogera.JSON_PROPERTY_CLASS
1612
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
17-
import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation
1813
import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass
1914
import io.github.projectmapk.jackson.module.kogera.jmClass.JmProperty
20-
import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter
2115
import io.github.projectmapk.jackson.module.kogera.reconstructClass
2216
import io.github.projectmapk.jackson.module.kogera.toSignature
23-
import kotlinx.metadata.KmClassifier
2417
import kotlinx.metadata.isNullable
2518
import java.lang.reflect.Constructor
26-
import java.lang.reflect.Executable
2719
import java.lang.reflect.Method
28-
import java.lang.reflect.Modifier
2920

3021
// AnnotationIntrospector that overrides the behavior of the default AnnotationIntrospector
3122
// (in most cases, JacksonAnnotationIntrospector).
@@ -116,57 +107,4 @@ internal class KotlinPrimaryAnnotationIntrospector(
116107
override fun findSubtypes(a: Annotated): List<NamedType>? = cache.getJmClass(a.rawType)?.let { jmClass ->
117108
jmClass.sealedSubclasses.map { NamedType(it.reconstructClass()) }.ifEmpty { null }
118109
}
119-
120-
// Return Mode.DEFAULT if ann is a Primary Constructor and the condition is satisfied.
121-
// Currently, there is no way to define the priority of a Creator,
122-
// so the presence or absence of a JsonCreator is included in the decision.
123-
// The reason for overriding the JacksonAnnotationIntrospector is to reduce overhead.
124-
// In rare cases, a problem may occur,
125-
// but it is assumed that the problem can be solved by adjusting the order of module registration.
126-
override fun findCreatorAnnotation(config: MapperConfig<*>, ann: Annotated): JsonCreator.Mode? {
127-
(ann as? AnnotatedConstructor)?.takeIf { 0 < it.parameterCount } ?: return null
128-
129-
val declaringClass = ann.declaringClass
130-
val jmClass = declaringClass.takeIf { !it.isEnum }
131-
?.let { cache.getJmClass(it) }
132-
?: return null
133-
134-
return JsonCreator.Mode.DEFAULT
135-
.takeIf { ann.annotated.isPrimarilyConstructorOf(jmClass) && !hasCreator(declaringClass, jmClass) }
136-
}
137-
}
138-
139-
private fun Constructor<*>.isPrimarilyConstructorOf(jmClass: JmClass): Boolean = jmClass.findJmConstructor(this)
140-
?.let { !it.isSecondary || jmClass.constructors.size == 1 }
141-
?: false
142-
143-
private fun KmClassifier.isString(): Boolean = this is KmClassifier.Class && this.name == "kotlin/String"
144-
145-
private fun isPossibleSingleString(
146-
kotlinParams: List<JmValueParameter>,
147-
javaFunction: Executable,
148-
propertyNames: Set<String>
149-
): Boolean = kotlinParams.size == 1 &&
150-
kotlinParams[0].let { it.name !in propertyNames && it.isString } &&
151-
javaFunction.parameters[0].annotations.none { it is JsonProperty }
152-
153-
private fun hasCreatorConstructor(clazz: Class<*>, jmClass: JmClass, propertyNames: Set<String>): Boolean {
154-
val kmConstructorMap = jmClass.constructors.associateBy { it.signature?.descriptor }
155-
156-
return clazz.constructors.any { constructor ->
157-
val kmConstructor = kmConstructorMap[constructor.toSignature().descriptor] ?: return@any false
158-
159-
!isPossibleSingleString(kmConstructor.valueParameters, constructor, propertyNames) &&
160-
constructor.hasCreatorAnnotation()
161-
}
162-
}
163-
164-
// In the original, `isPossibleSingleString` comparison was disabled,
165-
// and if enabled, the behavior would have changed, so the comparison is skipped.
166-
private fun hasCreatorFunction(clazz: Class<*>): Boolean = clazz.declaredMethods
167-
.any { Modifier.isStatic(it.modifiers) && it.hasCreatorAnnotation() }
168-
169-
private fun hasCreator(clazz: Class<*>, jmClass: JmClass): Boolean {
170-
val propertyNames = jmClass.propertyNameSet
171-
return hasCreatorConstructor(clazz, jmClass, propertyNames) || hasCreatorFunction(clazz)
172110
}

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

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.github.projectmapk.jackson.module.kogera.jmClass
22

33
import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull
4-
import io.github.projectmapk.jackson.module.kogera.toDescBuilder
54
import io.github.projectmapk.jackson.module.kogera.toKmClass
65
import io.github.projectmapk.jackson.module.kogera.toSignature
76
import kotlinx.metadata.ClassKind
@@ -106,25 +105,8 @@ private class JmClassImpl(
106105
companionPropName?.let { JmClass.CompanionObject(clazz, it) }
107106
}
108107

109-
override fun findJmConstructor(constructor: Constructor<*>): JmConstructor? {
110-
val descHead = constructor.parameterTypes.toDescBuilder()
111-
val len = descHead.length
112-
val desc = CharArray(len + 1).apply {
113-
descHead.getChars(0, len, this, 0)
114-
this[len] = 'V'
115-
}.let { String(it) }
116-
117-
// Only constructors that take a value class as an argument have a DefaultConstructorMarker on the Signature.
118-
val valueDesc = descHead
119-
.replace(len - 1, len, "Lkotlin/jvm/internal/DefaultConstructorMarker;)V")
120-
.toString()
121-
122-
// Constructors always have the same name, so only desc is compared
123-
return constructors.find {
124-
val targetDesc = it.signature?.descriptor
125-
targetDesc == desc || targetDesc == valueDesc
126-
}
127-
}
108+
override fun findJmConstructor(constructor: Constructor<*>): JmConstructor? =
109+
constructors.find { it.isMetadataFor(constructor) }
128110

129111
// Field name always matches property name
130112
override fun findPropertyByField(field: Field): JmProperty? = allPropsMap[field.name]

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package io.github.projectmapk.jackson.module.kogera.jmClass
22

3+
import io.github.projectmapk.jackson.module.kogera.toDescBuilder
34
import kotlinx.metadata.KmConstructor
45
import kotlinx.metadata.isSecondary
56
import kotlinx.metadata.jvm.JvmMethodSignature
67
import kotlinx.metadata.jvm.signature
8+
import java.lang.reflect.Constructor
79

810
internal data class JmConstructor(
911
val isSecondary: Boolean,
@@ -15,4 +17,22 @@ internal data class JmConstructor(
1517
signature = constructor.signature,
1618
valueParameters = constructor.valueParameters.map { JmValueParameter(it) }
1719
)
20+
21+
// Only constructors that take a value class as an argument have a DefaultConstructorMarker on the Signature.
22+
private fun StringBuilder.valueDesc(len: Int) =
23+
replace(len - 1, len, "Lkotlin/jvm/internal/DefaultConstructorMarker;)V").toString()
24+
25+
fun isMetadataFor(constructor: Constructor<*>): Boolean {
26+
val targetDesc = signature?.descriptor
27+
28+
val descHead = constructor.parameterTypes.toDescBuilder()
29+
val len = descHead.length
30+
val desc = CharArray(len + 1).apply {
31+
descHead.getChars(0, len, this, 0)
32+
this[len] = 'V'
33+
}.let { String(it) }
34+
35+
// Constructors always have the same name, so only desc is compared
36+
return targetDesc == desc || targetDesc == descHead.valueDesc(len)
37+
}
1838
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.github.projectmapk.jackson.module.kogera.zPorted.test.github
2+
3+
import com.fasterxml.jackson.databind.json.JsonMapper
4+
import io.github.projectmapk.jackson.module.kogera.KotlinFeature
5+
import io.github.projectmapk.jackson.module.kogera.KotlinModule
6+
import io.github.projectmapk.jackson.module.kogera.convertValue
7+
import org.junit.jupiter.api.Assertions.assertNull
8+
import org.junit.jupiter.api.Test
9+
10+
class GitHub757 {
11+
@Test
12+
fun test() {
13+
val kotlinModule = KotlinModule.Builder()
14+
.enable(KotlinFeature.StrictNullChecks)
15+
.build()
16+
val mapper = JsonMapper.builder()
17+
.addModule(kotlinModule)
18+
.build()
19+
val convertValue = mapper.convertValue<String?>(null)
20+
assertNull(convertValue)
21+
}
22+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.github.projectmapk.jackson.module.kogera.zPorted.test.github
2+
3+
import com.fasterxml.jackson.annotation.JsonTypeInfo
4+
import com.fasterxml.jackson.databind.ObjectMapper
5+
import io.github.projectmapk.jackson.module.kogera.readValue
6+
import io.github.projectmapk.jackson.module.kogera.registerKotlinModule
7+
import org.junit.jupiter.api.Assertions.assertEquals
8+
import org.junit.jupiter.api.Test
9+
10+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "_type")
11+
private sealed class BaseClass
12+
13+
private data class ChildClass(val text: String) : BaseClass()
14+
15+
class GitHub844 {
16+
@Test
17+
fun test() {
18+
val json = """
19+
{
20+
"_type": "ChildClass",
21+
"text": "Test"
22+
}
23+
"""
24+
25+
val jacksonObjectMapper = ObjectMapper().registerKotlinModule()
26+
val message = jacksonObjectMapper.readValue<BaseClass>(json)
27+
28+
assertEquals(ChildClass("Test"), message)
29+
}
30+
}

0 commit comments

Comments
 (0)