Skip to content

Commit d82a607

Browse files
committed
Add KeyDeserializer for value class
1 parent 2c41387 commit d82a607

File tree

2 files changed

+74
-9
lines changed

2 files changed

+74
-9
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public class KotlinModule private constructor(
7979
_deserializers = KotlinDeserializers(cache, useJavaDurationConversion)
8080

8181
_keySerializers = KotlinKeySerializers(cache)
82-
_keyDeserializers = KotlinKeyDeserializers
82+
_keyDeserializers = KotlinKeyDeserializers(cache)
8383

8484
_abstractTypes = ClosedRangeResolver
8585

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@ import com.fasterxml.jackson.databind.BeanDescription
44
import com.fasterxml.jackson.databind.DeserializationConfig
55
import com.fasterxml.jackson.databind.DeserializationContext
66
import com.fasterxml.jackson.databind.JavaType
7+
import com.fasterxml.jackson.databind.JsonMappingException
78
import com.fasterxml.jackson.databind.KeyDeserializer
89
import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializer
10+
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
911
import com.fasterxml.jackson.databind.module.SimpleKeyDeserializers
12+
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
13+
import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter
14+
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
15+
import io.github.projectmapk.jackson.module.kogera.toSignature
16+
import kotlinx.metadata.isSecondary
17+
import kotlinx.metadata.jvm.signature
18+
import java.lang.reflect.Method
1019
import java.math.BigInteger
1120

1221
// The reason why key is treated as nullable is to match the tentative behavior of StdKeyDeserializer.
@@ -40,18 +49,74 @@ internal object ULongKeyDeserializer : StdKeyDeserializer(-1, ULong::class.java)
4049
key?.let { ULongChecker.readWithRangeCheck(null, BigInteger(it)) }
4150
}
4251

43-
internal object KotlinKeyDeserializers : SimpleKeyDeserializers() {
44-
private fun readResolve(): Any = KotlinKeyDeserializers
52+
// The implementation is designed to be compatible with various creators, just in case.
53+
internal class ValueClassKeyDeserializer<S, D : Any>(
54+
private val creator: Method,
55+
private val converter: ValueClassBoxConverter<S, D>
56+
) : KeyDeserializer() {
57+
private val unboxedClass: Class<*> = creator.parameterTypes[0]
4558

59+
init {
60+
creator.apply { if (!this.isAccessible) this.isAccessible = true }
61+
}
62+
63+
// Based on databind error
64+
// https://github.com/FasterXML/jackson-databind/blob/341f8d360a5f10b5e609d6ee0ea023bf597ce98a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java#L624
65+
private fun errorMessage(boxedType: JavaType): String =
66+
"Could not find (Map) Key deserializer for types wrapped in $boxedType"
67+
68+
override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any {
69+
val unboxedJavaType = ctxt.constructType(unboxedClass)
70+
71+
return try {
72+
// findKeyDeserializer does not return null, and an exception will be thrown if not found.
73+
val value = ctxt.findKeyDeserializer(unboxedJavaType, null).deserializeKey(key, ctxt)
74+
@Suppress("UNCHECKED_CAST")
75+
converter.convert(creator.invoke(null, value) as S)
76+
} catch (e: InvalidDefinitionException) {
77+
throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass)), e)
78+
}
79+
}
80+
81+
companion object {
82+
fun createOrNull(
83+
valueClass: Class<*>,
84+
cache: ReflectionCache
85+
): ValueClassKeyDeserializer<*, *>? {
86+
val jmClass = cache.getJmClass(valueClass) ?: return null
87+
val primaryKmConstructorSignature =
88+
jmClass.constructors.first { !it.isSecondary }.signature
89+
90+
// Only primary constructor is allowed as creator, regardless of visibility.
91+
// This is because it is based on the WrapsNullableValueClassBoxDeserializer.
92+
// Also, as far as I could research, there was no such functionality as JsonKeyCreator,
93+
// so it was not taken into account.
94+
return valueClass.declaredMethods.find { it.toSignature() == primaryKmConstructorSignature }?.let {
95+
val unboxedClass = it.returnType
96+
97+
val converter = cache.getValueClassBoxConverter(unboxedClass, valueClass)
98+
99+
ValueClassKeyDeserializer(it, converter)
100+
}
101+
}
102+
}
103+
}
104+
105+
internal class KotlinKeyDeserializers(private val cache: ReflectionCache) : SimpleKeyDeserializers() {
46106
override fun findKeyDeserializer(
47107
type: JavaType,
48108
config: DeserializationConfig?,
49109
beanDesc: BeanDescription?
50-
): KeyDeserializer? = when (type.rawClass) {
51-
UByte::class.java -> UByteKeyDeserializer
52-
UShort::class.java -> UShortKeyDeserializer
53-
UInt::class.java -> UIntKeyDeserializer
54-
ULong::class.java -> ULongKeyDeserializer
55-
else -> null
110+
): KeyDeserializer? {
111+
val rawClass = type.rawClass
112+
113+
return when {
114+
rawClass == UByte::class.java -> UByteKeyDeserializer
115+
rawClass == UShort::class.java -> UShortKeyDeserializer
116+
rawClass == UInt::class.java -> UIntKeyDeserializer
117+
rawClass == ULong::class.java -> ULongKeyDeserializer
118+
rawClass.isUnboxableValueClass() -> ValueClassKeyDeserializer.createOrNull(rawClass, cache)
119+
else -> null
120+
}
56121
}
57122
}

0 commit comments

Comments
 (0)