Skip to content

Commit 002130c

Browse files
committed
Merge branch 'master' into tatu/3.0/887-use-module-info
2 parents 3a72206 + 3b7adfa commit 002130c

File tree

26 files changed

+937
-70
lines changed

26 files changed

+937
-70
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.fasterxml.jackson.module/jackson-module-kotlin/badge.svg)](https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-kotlin)
22
[![Change log](https://img.shields.io/badge/change%20log-%E2%96%A4-yellow.svg)](./release-notes/VERSION-2.x)
3+
[![Tidelift](https://tidelift.com/badges/package/maven/com.fasterxml.jackson.module:jackson-module-kotlin)](https://tidelift.com/subscription/pkg/maven-com-fasterxml-jackson-module-jackson-module-kotlin?utm_source=maven-com-fasterxml-jackson-module-jackson-module-kotlin&utm_medium=referral&utm_campaign=readme)
34
[![Kotlin Slack](https://img.shields.io/badge/chat-kotlin%20slack-orange.svg)](https://slack.kotlinlang.org/)
45

56
# Overview

release-notes/CREDITS-2.x

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Contributors:
1818
# 2.19.0 (not yet released)
1919

2020
WrongWrong (@k163377)
21+
* #910: Add default KeyDeserializer for value class
2122
* #885: Performance improvement of strictNullChecks
2223
* #884: Changed the base class of MissingKotlinParameterException to InvalidNullException
2324
* #878: Fix for #876
@@ -28,6 +29,13 @@ WrongWrong (@k163377)
2829
* #839: Remove useKotlinPropertyNameForGetter and unify with kotlinPropertyNameAsImplicitName
2930
* #835: Remove old SingletonSupport class and unified with KotlinFeature.SingletonSupport
3031

32+
# 2.18.3 (not yet released)
33+
34+
WrongWrong (@k163377)
35+
* #908: Additional fixes related to #904.
36+
* #904: Fixed an error when serializing a `value class` that wraps a `Map`
37+
* #900: Fixed an issue where some tests were not running
38+
3139
# 2.18.0 (26-Sep-2024)
3240

3341
WrongWrong (@k163377)

release-notes/VERSION-2.x

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Co-maintainers:
1818

1919
2.19.0 (not yet released)
2020

21+
#910: A default `KeySerializer` for `value class` has been added.
22+
This eliminates the need to have a custom `KeySerializer` for each `value class` when using it as a key in a `Map`, if only simple boxing is needed.
2123
#889: Kotlin has been upgraded to 1.9.25.
2224
#885: A new `StrictNullChecks` option(KotlinFeature.NewStrictNullChecks) has been added which greatly improves throughput.
2325
Benchmarks show a consistent throughput drop of less than 2% when enabled (prior to the improvement, the worst throughput drop was more than 30%).
@@ -34,6 +36,12 @@ Co-maintainers:
3436
#839: Remove useKotlinPropertyNameForGetter and unify with kotlinPropertyNameAsImplicitName.
3537
#835: Remove old SingletonSupport class and unified with KotlinFeature.SingletonSupport.
3638

39+
2.18.3 (not yet released)
40+
41+
#904: Fixed a problem where context was not being propagated properly when serializing an unboxed value of `value class`
42+
or a value retrieved with `JsonValue`.
43+
This fixes a problem where an error would occur when serializing a `value class` that wraps a `Map`(#873).
44+
3745
2.18.2 (27-Nov-2024)
3846
2.18.1 (28-Oct-2024)
3947

src/main/kotlin/tools/jackson/module/kotlin/KotlinKeyDeserializers.kt

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import tools.jackson.core.exc.InputCoercionException
55
import tools.jackson.databind.*
66
import tools.jackson.databind.deser.jdk.JDKKeyDeserializer
77
import tools.jackson.databind.deser.jdk.JDKKeyDeserializers
8+
import tools.jackson.databind.exc.InvalidDefinitionException
9+
import java.lang.reflect.Method
10+
import kotlin.reflect.KClass
11+
import kotlin.reflect.full.primaryConstructor
12+
import kotlin.reflect.jvm.javaMethod
813

914
// The reason why key is treated as nullable is to match the tentative behavior of JDKKeyDeserializer.
1015
// If JDKKeyDeserializer is modified, need to modify this too.
@@ -57,16 +62,68 @@ internal object ULongKeyDeserializer : JDKKeyDeserializer(TYPE_LONG, ULong::clas
5762
}
5863
}
5964

60-
internal object KotlinKeyDeserializers : JDKKeyDeserializers() {
65+
// The implementation is designed to be compatible with various creators, just in case.
66+
internal class ValueClassKeyDeserializer<S, D : Any>(
67+
private val creator: Method,
68+
private val converter: ValueClassBoxConverter<S, D>
69+
) : KeyDeserializer() {
70+
private val unboxedClass: Class<*> = creator.parameterTypes[0]
71+
72+
init {
73+
creator.apply { if (!this.isAccessible) this.isAccessible = true }
74+
}
75+
76+
// Based on databind error
77+
// https://github.com/FasterXML/jackson-databind/blob/341f8d360a5f10b5e609d6ee0ea023bf597ce98a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java#L624
78+
private fun errorMessage(boxedType: JavaType): String =
79+
"Could not find (Map) Key deserializer for types wrapped in $boxedType"
80+
81+
override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any {
82+
val unboxedJavaType = ctxt.constructType(unboxedClass)
83+
84+
return try {
85+
// findKeyDeserializer does not return null, and an exception will be thrown if not found.
86+
val value = ctxt.findKeyDeserializer(unboxedJavaType, null).deserializeKey(key, ctxt)
87+
@Suppress("UNCHECKED_CAST")
88+
converter.convert(creator.invoke(null, value) as S)
89+
} catch (e: InvalidDefinitionException) {
90+
throw DatabindException.from(ctxt.parser, errorMessage(ctxt.constructType(converter.boxedClass.java)), e)
91+
}
92+
}
93+
94+
companion object {
95+
fun createOrNull(
96+
boxedClass: KClass<*>,
97+
cache: ReflectionCache
98+
): ValueClassKeyDeserializer<*, *>? {
99+
// primaryConstructor.javaMethod for the value class returns constructor-impl
100+
// Only primary constructor is allowed as creator, regardless of visibility.
101+
// This is because it is based on the WrapsNullableValueClassBoxDeserializer.
102+
// Also, as far as I could research, there was no such functionality as JsonKeyCreator,
103+
// so it was not taken into account.
104+
val creator = boxedClass.primaryConstructor?.javaMethod ?: return null
105+
val converter = cache.getValueClassBoxConverter(creator.returnType, boxedClass)
106+
107+
return ValueClassKeyDeserializer(creator, converter)
108+
}
109+
}
110+
}
111+
112+
internal class KotlinKeyDeserializers(private val cache: ReflectionCache) : JDKKeyDeserializers() {
61113
override fun findKeyDeserializer(
62114
type: JavaType,
63115
config: DeserializationConfig?,
64116
beanDesc: BeanDescription?,
65-
): KeyDeserializer? = when (type.rawClass) {
66-
UByte::class.java -> UByteKeyDeserializer
67-
UShort::class.java -> UShortKeyDeserializer
68-
UInt::class.java -> UIntKeyDeserializer
69-
ULong::class.java -> ULongKeyDeserializer
70-
else -> null
117+
): KeyDeserializer? {
118+
val rawClass = type.rawClass
119+
120+
return when {
121+
rawClass == UByte::class.java -> UByteKeyDeserializer
122+
rawClass == UShort::class.java -> UShortKeyDeserializer
123+
rawClass == UInt::class.java -> UIntKeyDeserializer
124+
rawClass == ULong::class.java -> ULongKeyDeserializer
125+
rawClass.isUnboxableValueClass() -> ValueClassKeyDeserializer.createOrNull(rawClass.kotlin, cache)
126+
else -> null
127+
}
71128
}
72129
}

src/main/kotlin/tools/jackson/module/kotlin/KotlinModule.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ fun Class<*>.isKotlinClass(): Boolean = this.isAnnotationPresent(Metadata::class
1919
* using the default value provided in Kotlin.
2020
* @property singletonSupport Default: false. Mode for singleton handling.
2121
* See [KotlinFeature.SingletonSupport]
22-
* @property enabledSingletonSupport Default: false. A temporary property that is maintained until the return value of `singletonSupport` is changed.
23-
* It will be removed in 2.21.
2422
* @property strictNullChecks Default: false. Whether to check deserialized collections. With this disabled,
2523
* the default, collections which are typed to disallow null members
2624
* (e.g. List<String>) may contain null values after deserialization. Enabling it
@@ -107,7 +105,7 @@ class KotlinModule private constructor(
107105
)
108106

109107
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
110-
context.addKeyDeserializers(KotlinKeyDeserializers)
108+
context.addKeyDeserializers(KotlinKeyDeserializers(cache))
111109
context.addSerializers(KotlinSerializers())
112110
context.addKeySerializers(KotlinKeySerializers())
113111

src/main/kotlin/tools/jackson/module/kotlin/KotlinSerializers.kt

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,7 @@ private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods.
5050
object ValueClassUnboxSerializer : StdSerializer<Any>(Any::class.java) {
5151
override fun serialize(value: Any, gen: JsonGenerator, ctxt: SerializationContext) {
5252
val unboxed = value::class.java.getMethod("unbox-impl").invoke(value)
53-
54-
if (unboxed == null) {
55-
ctxt.findNullValueSerializer(null).serialize(null, gen, ctxt)
56-
return
57-
}
58-
59-
ctxt.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, ctxt)
53+
ctxt.writeValue(gen, unboxed)
6054
}
6155
}
6256

@@ -70,9 +64,7 @@ internal sealed class ValueClassSerializer<T : Any>(t: Class<T>) : StdSerializer
7064
val unboxed = unboxMethod.invoke(value)
7165
// As shown in the processing of the factory function, jsonValueGetter is always a static method.
7266
val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed)
73-
jsonValue
74-
?.let { ctxt.findValueSerializer(it::class.java).serialize(it, gen, ctxt) }
75-
?: ctxt.findNullValueSerializer(null).serialize(null, gen, ctxt)
67+
ctxt.writeValue(gen, jsonValue)
7668
}
7769
}
7870

src/test/kotlin/tools/jackson/module/kotlin/kogeraIntegration/deser/StrictNullChecksTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser
1+
package tools.jackson.module.kotlin.kogeraIntegration.deser
22

33
import com.fasterxml.jackson.annotation.JsonSetter
44
import com.fasterxml.jackson.annotation.Nulls

src/test/kotlin/tools/jackson/module/kotlin/kogeraIntegration/deser/valueClass/ValueClasses.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import tools.jackson.core.JsonParser
44
import tools.jackson.databind.DeserializationContext
55
import tools.jackson.databind.deser.std.StdDeserializer
66
import tools.jackson.module.kotlin.WrapsNullableValueClassDeserializer
7+
import tools.jackson.databind.KeyDeserializer as JacksonKeyDeserializer
78

89
@JvmInline
910
value class Primitive(val v: Int) {
1011
class Deserializer : StdDeserializer<Primitive>(Primitive::class.java) {
1112
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Primitive = Primitive(p.intValue + 100)
1213
}
14+
15+
class KeyDeserializer : JacksonKeyDeserializer() {
16+
override fun deserializeKey(key: String, ctxt: DeserializationContext) = Primitive(key.toInt() + 100)
17+
}
1318
}
1419

1520
@JvmInline
@@ -18,6 +23,10 @@ value class NonNullObject(val v: String) {
1823
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): NonNullObject =
1924
NonNullObject(p.valueAsString + "-deser")
2025
}
26+
27+
class KeyDeserializer : JacksonKeyDeserializer() {
28+
override fun deserializeKey(key: String, ctxt: DeserializationContext) = NonNullObject("$key-deser")
29+
}
2130
}
2231

2332
@JvmInline
@@ -28,4 +37,8 @@ value class NullableObject(val v: String?) {
2837

2938
override fun getBoxedNullValue(): NullableObject = NullableObject("null-value-deser")
3039
}
40+
41+
class KeyDeserializer : JacksonKeyDeserializer() {
42+
override fun deserializeKey(key: String, ctxt: DeserializationContext) = NullableObject("$key-deser")
43+
}
3144
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey
2+
3+
import tools.jackson.module.kotlin.defaultMapper
4+
import tools.jackson.module.kotlin.readValue
5+
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NonNullObject
6+
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NullableObject
7+
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.Primitive
8+
import org.junit.jupiter.api.Assertions.assertEquals
9+
import org.junit.jupiter.api.Assertions.assertTrue
10+
import org.junit.jupiter.api.Nested
11+
import org.junit.jupiter.api.Test
12+
import org.junit.jupiter.api.assertThrows
13+
import tools.jackson.databind.DatabindException
14+
import tools.jackson.databind.DeserializationContext
15+
import tools.jackson.databind.exc.InvalidDefinitionException
16+
import tools.jackson.databind.module.SimpleModule
17+
import tools.jackson.module.kotlin.jacksonMapperBuilder
18+
import java.lang.reflect.InvocationTargetException
19+
import tools.jackson.databind.KeyDeserializer as JacksonKeyDeserializer
20+
21+
class WithoutCustomDeserializeMethodTest {
22+
companion object {
23+
val throwable = IllegalArgumentException("test")
24+
}
25+
26+
@Nested
27+
inner class DirectDeserialize {
28+
@Test
29+
fun primitive() {
30+
val result = defaultMapper.readValue<Map<Primitive, String?>>("""{"1":null}""")
31+
assertEquals(mapOf(Primitive(1) to null), result)
32+
}
33+
34+
@Test
35+
fun nonNullObject() {
36+
val result = defaultMapper.readValue<Map<NonNullObject, String?>>("""{"foo":null}""")
37+
assertEquals(mapOf(NonNullObject("foo") to null), result)
38+
}
39+
40+
@Test
41+
fun nullableObject() {
42+
val result = defaultMapper.readValue<Map<NullableObject, String?>>("""{"bar":null}""")
43+
assertEquals(mapOf(NullableObject("bar") to null), result)
44+
}
45+
}
46+
47+
data class Dst(
48+
val p: Map<Primitive, String?>,
49+
val nn: Map<NonNullObject, String?>,
50+
val n: Map<NullableObject, String?>
51+
)
52+
53+
@Test
54+
fun wrapped() {
55+
val src = """
56+
{
57+
"p":{"1":null},
58+
"nn":{"foo":null},
59+
"n":{"bar":null}
60+
}
61+
""".trimIndent()
62+
val result = defaultMapper.readValue<Dst>(src)
63+
val expected = Dst(
64+
mapOf(Primitive(1) to null),
65+
mapOf(NonNullObject("foo") to null),
66+
mapOf(NullableObject("bar") to null)
67+
)
68+
69+
assertEquals(expected, result)
70+
}
71+
72+
@JvmInline
73+
value class HasCheckConstructor(val value: Int) {
74+
init {
75+
if (value < 0) throw throwable
76+
}
77+
}
78+
79+
@Test
80+
fun callConstructorCheckTest() {
81+
val e = assertThrows<InvocationTargetException> {
82+
defaultMapper.readValue<Map<HasCheckConstructor, String?>>("""{"-1":null}""")
83+
}
84+
assertTrue(e.cause === throwable)
85+
}
86+
87+
data class Wrapped(val first: String, val second: String) {
88+
class KeyDeserializer : JacksonKeyDeserializer() {
89+
override fun deserializeKey(key: String, ctxt: DeserializationContext) =
90+
key.split("-").let { Wrapped(it[0], it[1]) }
91+
}
92+
}
93+
94+
@JvmInline
95+
value class Wrapper(val w: Wrapped)
96+
97+
@Test
98+
fun wrappedCustomObject() {
99+
// If a type that cannot be deserialized is specified, the default is an error.
100+
val thrown = assertThrows<DatabindException> {
101+
defaultMapper.readValue<Map<Wrapper, String?>>("""{"foo-bar":null}""")
102+
}
103+
assertTrue(thrown.cause is InvalidDefinitionException)
104+
105+
val mapper = jacksonMapperBuilder()
106+
.addModule(
107+
object : SimpleModule() {
108+
init { addKeyDeserializer(Wrapped::class.java, Wrapped.KeyDeserializer()) }
109+
}
110+
)
111+
.build()
112+
113+
val result = mapper.readValue<Map<Wrapper, String?>>("""{"foo-bar":null}""")
114+
val expected = mapOf(Wrapper(Wrapped("foo", "bar")) to null)
115+
116+
assertEquals(expected, result)
117+
}
118+
}

0 commit comments

Comments
 (0)