Skip to content

Commit 152bccf

Browse files
committed
Add KeyDeserializer for value class
based ProjectMapK/jackson-module-kogera#224
1 parent 98df422 commit 152bccf

File tree

2 files changed

+64
-9
lines changed

2 files changed

+64
-9
lines changed

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

Lines changed: 63 additions & 8 deletions
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

Lines changed: 1 addition & 1 deletion
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

0 commit comments

Comments
 (0)