Skip to content

Commit 1622a77

Browse files
authored
Merge pull request #910 from k163377/777
Add default `KeyDeserializer` for `value class`
2 parents 98df422 + cf3a779 commit 1622a77

File tree

10 files changed

+333
-9
lines changed

10 files changed

+333
-9
lines changed

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@
251251
<exclude>
252252
com.fasterxml.jackson.module.kotlin.KotlinNamesAnnotationIntrospector#KotlinNamesAnnotationIntrospector(com.fasterxml.jackson.module.kotlin.ReflectionCache,boolean)
253253
</exclude>
254+
<exclude>com.fasterxml.jackson.module.kotlin.KotlinKeyDeserializers#INSTANCE</exclude>
254255
</excludes>
255256
</parameter>
256257
</configuration>

release-notes/CREDITS-2.x

+1
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

release-notes/VERSION-2.x

+2
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%).

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt

+63-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import com.fasterxml.jackson.core.exc.InputCoercionException
55
import com.fasterxml.jackson.databind.*
66
import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializer
77
import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializers
8+
import com.fasterxml.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 StdKeyDeserializer.
1015
// If StdKeyDeserializer is modified, need to modify this too.
@@ -65,18 +70,68 @@ internal object ULongKeyDeserializer : StdKeyDeserializer(TYPE_LONG, ULong::clas
6570
}
6671
}
6772

68-
internal object KotlinKeyDeserializers : StdKeyDeserializers() {
69-
private fun readResolve(): Any = KotlinKeyDeserializers
73+
// The implementation is designed to be compatible with various creators, just in case.
74+
internal class ValueClassKeyDeserializer<S, D : Any>(
75+
private val creator: Method,
76+
private val converter: ValueClassBoxConverter<S, D>
77+
) : KeyDeserializer() {
78+
private val unboxedClass: Class<*> = creator.parameterTypes[0]
7079

80+
init {
81+
creator.apply { if (!this.isAccessible) this.isAccessible = true }
82+
}
83+
84+
// Based on databind error
85+
// https://github.com/FasterXML/jackson-databind/blob/341f8d360a5f10b5e609d6ee0ea023bf597ce98a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java#L624
86+
private fun errorMessage(boxedType: JavaType): String =
87+
"Could not find (Map) Key deserializer for types wrapped in $boxedType"
88+
89+
override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any {
90+
val unboxedJavaType = ctxt.constructType(unboxedClass)
91+
92+
return try {
93+
// findKeyDeserializer does not return null, and an exception will be thrown if not found.
94+
val value = ctxt.findKeyDeserializer(unboxedJavaType, null).deserializeKey(key, ctxt)
95+
@Suppress("UNCHECKED_CAST")
96+
converter.convert(creator.invoke(null, value) as S)
97+
} catch (e: InvalidDefinitionException) {
98+
throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass.java)), e)
99+
}
100+
}
101+
102+
companion object {
103+
fun createOrNull(
104+
boxedClass: KClass<*>,
105+
cache: ReflectionCache
106+
): ValueClassKeyDeserializer<*, *>? {
107+
// primaryConstructor.javaMethod for the value class returns constructor-impl
108+
// Only primary constructor is allowed as creator, regardless of visibility.
109+
// This is because it is based on the WrapsNullableValueClassBoxDeserializer.
110+
// Also, as far as I could research, there was no such functionality as JsonKeyCreator,
111+
// so it was not taken into account.
112+
val creator = boxedClass.primaryConstructor?.javaMethod ?: return null
113+
val converter = cache.getValueClassBoxConverter(creator.returnType, boxedClass)
114+
115+
return ValueClassKeyDeserializer(creator, converter)
116+
}
117+
}
118+
}
119+
120+
internal class KotlinKeyDeserializers(private val cache: ReflectionCache) : StdKeyDeserializers() {
71121
override fun findKeyDeserializer(
72122
type: JavaType,
73123
config: DeserializationConfig?,
74124
beanDesc: BeanDescription?
75-
): KeyDeserializer? = when (type.rawClass) {
76-
UByte::class.java -> UByteKeyDeserializer
77-
UShort::class.java -> UShortKeyDeserializer
78-
UInt::class.java -> UIntKeyDeserializer
79-
ULong::class.java -> ULongKeyDeserializer
80-
else -> null
125+
): KeyDeserializer? {
126+
val rawClass = type.rawClass
127+
128+
return when {
129+
rawClass == UByte::class.java -> UByteKeyDeserializer
130+
rawClass == UShort::class.java -> UShortKeyDeserializer
131+
rawClass == UInt::class.java -> UIntKeyDeserializer
132+
rawClass == ULong::class.java -> ULongKeyDeserializer
133+
rawClass.isUnboxableValueClass() -> ValueClassKeyDeserializer.createOrNull(rawClass.kotlin, cache)
134+
else -> null
135+
}
81136
}
82137
}

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ class KotlinModule private constructor(
130130
)
131131

132132
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
133-
context.addKeyDeserializers(KotlinKeyDeserializers)
133+
context.addKeyDeserializers(KotlinKeyDeserializers(cache))
134134
context.addSerializers(KotlinSerializers())
135135
context.addKeySerializers(KotlinKeySerializers())
136136

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

+13
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import com.fasterxml.jackson.core.JsonParser
44
import com.fasterxml.jackson.databind.DeserializationContext
55
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
66
import com.fasterxml.jackson.module.kotlin.WrapsNullableValueClassDeserializer
7+
import com.fasterxml.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
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey
2+
3+
import com.fasterxml.jackson.databind.DeserializationContext
4+
import com.fasterxml.jackson.databind.JsonMappingException
5+
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
6+
import com.fasterxml.jackson.databind.module.SimpleModule
7+
import com.fasterxml.jackson.module.kotlin.defaultMapper
8+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
9+
import com.fasterxml.jackson.module.kotlin.readValue
10+
import com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NonNullObject
11+
import com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NullableObject
12+
import com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.Primitive
13+
import org.junit.jupiter.api.Assertions.assertEquals
14+
import org.junit.jupiter.api.Assertions.assertTrue
15+
import org.junit.jupiter.api.Nested
16+
import org.junit.jupiter.api.Test
17+
import org.junit.jupiter.api.assertThrows
18+
import java.lang.reflect.InvocationTargetException
19+
import com.fasterxml.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<JsonMappingException> {
101+
defaultMapper.readValue<Map<Wrapper, String?>>("""{"foo-bar":null}""")
102+
}
103+
assertTrue(thrown.cause is InvalidDefinitionException)
104+
105+
val mapper = jacksonObjectMapper()
106+
.registerModule(
107+
object : SimpleModule() {
108+
init { addKeyDeserializer(Wrapped::class.java, Wrapped.KeyDeserializer()) }
109+
}
110+
)
111+
112+
val result = mapper.readValue<Map<Wrapper, String?>>("""{"foo-bar":null}""")
113+
val expected = mapOf(Wrapper(Wrapped("foo", "bar")) to null)
114+
115+
assertEquals(expected, result)
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey.keyDeserializer
2+
3+
import com.fasterxml.jackson.databind.module.SimpleModule
4+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5+
import com.fasterxml.jackson.module.kotlin.readValue
6+
import com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NonNullObject
7+
import com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NullableObject
8+
import com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.Primitive
9+
import org.junit.jupiter.api.Assertions.assertEquals
10+
import org.junit.jupiter.api.Nested
11+
import org.junit.jupiter.api.Test
12+
13+
class SpecifiedForObjectMapperTest {
14+
companion object {
15+
val mapper = jacksonObjectMapper().apply {
16+
val module = SimpleModule().apply {
17+
this.addKeyDeserializer(Primitive::class.java, Primitive.KeyDeserializer())
18+
this.addKeyDeserializer(NonNullObject::class.java, NonNullObject.KeyDeserializer())
19+
this.addKeyDeserializer(NullableObject::class.java, NullableObject.KeyDeserializer())
20+
}
21+
this.registerModule(module)
22+
}
23+
}
24+
25+
@Nested
26+
inner class DirectDeserialize {
27+
@Test
28+
fun primitive() {
29+
val result = mapper.readValue<Map<Primitive, String?>>("""{"1":null}""")
30+
assertEquals(mapOf(Primitive(101) to null), result)
31+
}
32+
33+
@Test
34+
fun nonNullObject() {
35+
val result = mapper.readValue<Map<NonNullObject, String?>>("""{"foo":null}""")
36+
assertEquals(mapOf(NonNullObject("foo-deser") to null), result)
37+
}
38+
39+
@Test
40+
fun nullableObject() {
41+
val result = mapper.readValue<Map<NullableObject, String?>>("""{"bar":null}""")
42+
assertEquals(mapOf(NullableObject("bar-deser") to null), result)
43+
}
44+
}
45+
46+
data class Dst(
47+
val p: Map<Primitive, String?>,
48+
val nn: Map<NonNullObject, String?>,
49+
val n: Map<NullableObject, String?>
50+
)
51+
52+
@Test
53+
fun wrapped() {
54+
val src = """
55+
{
56+
"p":{"1":null},
57+
"nn":{"foo":null},
58+
"n":{"bar":null}
59+
}
60+
""".trimIndent()
61+
val result = mapper.readValue<Dst>(src)
62+
val expected = Dst(
63+
mapOf(Primitive(101) to null),
64+
mapOf(NonNullObject("foo-deser") to null),
65+
mapOf(NullableObject("bar-deser") to null)
66+
)
67+
68+
assertEquals(expected, result)
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey.keyDeserializer.byAnnotation
2+
3+
import com.fasterxml.jackson.databind.DeserializationContext
4+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
5+
import com.fasterxml.jackson.module.kotlin.defaultMapper
6+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
7+
import com.fasterxml.jackson.module.kotlin.readValue
8+
import org.junit.jupiter.api.Assertions.assertEquals
9+
import org.junit.jupiter.api.Test
10+
import com.fasterxml.jackson.databind.KeyDeserializer as JacksonKeyDeserializer
11+
12+
class SpecifiedForClassTest {
13+
@JsonDeserialize(keyUsing = Value.KeyDeserializer::class)
14+
@JvmInline
15+
value class Value(val v: Int) {
16+
class KeyDeserializer : JacksonKeyDeserializer() {
17+
override fun deserializeKey(key: String, ctxt: DeserializationContext) = Value(key.toInt() + 100)
18+
}
19+
}
20+
21+
@Test
22+
fun directDeserTest() {
23+
val result = defaultMapper.readValue<Map<Value, String?>>("""{"1":null}""")
24+
25+
assertEquals(mapOf(Value(101) to null), result)
26+
}
27+
28+
data class Wrapper(val v: Map<Value, String?>)
29+
30+
@Test
31+
fun paramDeserTest() {
32+
val mapper = jacksonObjectMapper()
33+
val result = mapper.readValue<Wrapper>("""{"v":{"1":null}}""")
34+
35+
assertEquals(Wrapper(mapOf(Value(101) to null)), result)
36+
}
37+
}

0 commit comments

Comments
 (0)