Skip to content

Commit

Permalink
Merge remote-tracking branch 'FasterXML/2.19'
Browse files Browse the repository at this point in the history
  • Loading branch information
k163377 committed Feb 2, 2025
2 parents e7fdcf8 + 1622a77 commit 3b7adfa
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 8 deletions.
1 change: 1 addition & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Contributors:
# 2.19.0 (not yet released)

WrongWrong (@k163377)
* #910: Add default KeyDeserializer for value class
* #885: Performance improvement of strictNullChecks
* #884: Changed the base class of MissingKotlinParameterException to InvalidNullException
* #878: Fix for #876
Expand Down
2 changes: 2 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Co-maintainers:

2.19.0 (not yet released)

#910: A default `KeySerializer` for `value class` has been added.
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.
#889: Kotlin has been upgraded to 1.9.25.
#885: A new `StrictNullChecks` option(KotlinFeature.NewStrictNullChecks) has been added which greatly improves throughput.
Benchmarks show a consistent throughput drop of less than 2% when enabled (prior to the improvement, the worst throughput drop was more than 30%).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import tools.jackson.core.exc.InputCoercionException
import tools.jackson.databind.*
import tools.jackson.databind.deser.jdk.JDKKeyDeserializer
import tools.jackson.databind.deser.jdk.JDKKeyDeserializers
import tools.jackson.databind.exc.InvalidDefinitionException
import java.lang.reflect.Method
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaMethod

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

internal object KotlinKeyDeserializers : JDKKeyDeserializers() {
// The implementation is designed to be compatible with various creators, just in case.
internal class ValueClassKeyDeserializer<S, D : Any>(
private val creator: Method,
private val converter: ValueClassBoxConverter<S, D>
) : KeyDeserializer() {
private val unboxedClass: Class<*> = creator.parameterTypes[0]

init {
creator.apply { if (!this.isAccessible) this.isAccessible = true }
}

// Based on databind error
// https://github.com/FasterXML/jackson-databind/blob/341f8d360a5f10b5e609d6ee0ea023bf597ce98a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java#L624
private fun errorMessage(boxedType: JavaType): String =
"Could not find (Map) Key deserializer for types wrapped in $boxedType"

override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any {
val unboxedJavaType = ctxt.constructType(unboxedClass)

return try {
// findKeyDeserializer does not return null, and an exception will be thrown if not found.
val value = ctxt.findKeyDeserializer(unboxedJavaType, null).deserializeKey(key, ctxt)
@Suppress("UNCHECKED_CAST")
converter.convert(creator.invoke(null, value) as S)
} catch (e: InvalidDefinitionException) {
throw DatabindException.from(ctxt.parser, errorMessage(ctxt.constructType(converter.boxedClass.java)), e)
}
}

companion object {
fun createOrNull(
boxedClass: KClass<*>,
cache: ReflectionCache
): ValueClassKeyDeserializer<*, *>? {
// primaryConstructor.javaMethod for the value class returns constructor-impl
// Only primary constructor is allowed as creator, regardless of visibility.
// This is because it is based on the WrapsNullableValueClassBoxDeserializer.
// Also, as far as I could research, there was no such functionality as JsonKeyCreator,
// so it was not taken into account.
val creator = boxedClass.primaryConstructor?.javaMethod ?: return null
val converter = cache.getValueClassBoxConverter(creator.returnType, boxedClass)

return ValueClassKeyDeserializer(creator, converter)
}
}
}

internal class KotlinKeyDeserializers(private val cache: ReflectionCache) : JDKKeyDeserializers() {
override fun findKeyDeserializer(
type: JavaType,
config: DeserializationConfig?,
beanDesc: BeanDescription?,
): KeyDeserializer? = when (type.rawClass) {
UByte::class.java -> UByteKeyDeserializer
UShort::class.java -> UShortKeyDeserializer
UInt::class.java -> UIntKeyDeserializer
ULong::class.java -> ULongKeyDeserializer
else -> null
): KeyDeserializer? {
val rawClass = type.rawClass

return when {
rawClass == UByte::class.java -> UByteKeyDeserializer
rawClass == UShort::class.java -> UShortKeyDeserializer
rawClass == UInt::class.java -> UIntKeyDeserializer
rawClass == ULong::class.java -> ULongKeyDeserializer
rawClass.isUnboxableValueClass() -> ValueClassKeyDeserializer.createOrNull(rawClass.kotlin, cache)
else -> null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class KotlinModule private constructor(
)

context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
context.addKeyDeserializers(KotlinKeyDeserializers)
context.addKeyDeserializers(KotlinKeyDeserializers(cache))
context.addSerializers(KotlinSerializers())
context.addKeySerializers(KotlinKeySerializers())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import tools.jackson.core.JsonParser
import tools.jackson.databind.DeserializationContext
import tools.jackson.databind.deser.std.StdDeserializer
import tools.jackson.module.kotlin.WrapsNullableValueClassDeserializer
import tools.jackson.databind.KeyDeserializer as JacksonKeyDeserializer

@JvmInline
value class Primitive(val v: Int) {
class Deserializer : StdDeserializer<Primitive>(Primitive::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Primitive = Primitive(p.intValue + 100)
}

class KeyDeserializer : JacksonKeyDeserializer() {
override fun deserializeKey(key: String, ctxt: DeserializationContext) = Primitive(key.toInt() + 100)
}
}

@JvmInline
Expand All @@ -18,6 +23,10 @@ value class NonNullObject(val v: String) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): NonNullObject =
NonNullObject(p.valueAsString + "-deser")
}

class KeyDeserializer : JacksonKeyDeserializer() {
override fun deserializeKey(key: String, ctxt: DeserializationContext) = NonNullObject("$key-deser")
}
}

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

override fun getBoxedNullValue(): NullableObject = NullableObject("null-value-deser")
}

class KeyDeserializer : JacksonKeyDeserializer() {
override fun deserializeKey(key: String, ctxt: DeserializationContext) = NullableObject("$key-deser")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey

import tools.jackson.module.kotlin.defaultMapper
import tools.jackson.module.kotlin.readValue
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NonNullObject
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NullableObject
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.Primitive
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import tools.jackson.databind.DatabindException
import tools.jackson.databind.DeserializationContext
import tools.jackson.databind.exc.InvalidDefinitionException
import tools.jackson.databind.module.SimpleModule
import tools.jackson.module.kotlin.jacksonMapperBuilder
import java.lang.reflect.InvocationTargetException
import tools.jackson.databind.KeyDeserializer as JacksonKeyDeserializer

class WithoutCustomDeserializeMethodTest {
companion object {
val throwable = IllegalArgumentException("test")
}

@Nested
inner class DirectDeserialize {
@Test
fun primitive() {
val result = defaultMapper.readValue<Map<Primitive, String?>>("""{"1":null}""")
assertEquals(mapOf(Primitive(1) to null), result)
}

@Test
fun nonNullObject() {
val result = defaultMapper.readValue<Map<NonNullObject, String?>>("""{"foo":null}""")
assertEquals(mapOf(NonNullObject("foo") to null), result)
}

@Test
fun nullableObject() {
val result = defaultMapper.readValue<Map<NullableObject, String?>>("""{"bar":null}""")
assertEquals(mapOf(NullableObject("bar") to null), result)
}
}

data class Dst(
val p: Map<Primitive, String?>,
val nn: Map<NonNullObject, String?>,
val n: Map<NullableObject, String?>
)

@Test
fun wrapped() {
val src = """
{
"p":{"1":null},
"nn":{"foo":null},
"n":{"bar":null}
}
""".trimIndent()
val result = defaultMapper.readValue<Dst>(src)
val expected = Dst(
mapOf(Primitive(1) to null),
mapOf(NonNullObject("foo") to null),
mapOf(NullableObject("bar") to null)
)

assertEquals(expected, result)
}

@JvmInline
value class HasCheckConstructor(val value: Int) {
init {
if (value < 0) throw throwable
}
}

@Test
fun callConstructorCheckTest() {
val e = assertThrows<InvocationTargetException> {
defaultMapper.readValue<Map<HasCheckConstructor, String?>>("""{"-1":null}""")
}
assertTrue(e.cause === throwable)
}

data class Wrapped(val first: String, val second: String) {
class KeyDeserializer : JacksonKeyDeserializer() {
override fun deserializeKey(key: String, ctxt: DeserializationContext) =
key.split("-").let { Wrapped(it[0], it[1]) }
}
}

@JvmInline
value class Wrapper(val w: Wrapped)

@Test
fun wrappedCustomObject() {
// If a type that cannot be deserialized is specified, the default is an error.
val thrown = assertThrows<DatabindException> {
defaultMapper.readValue<Map<Wrapper, String?>>("""{"foo-bar":null}""")
}
assertTrue(thrown.cause is InvalidDefinitionException)

val mapper = jacksonMapperBuilder()
.addModule(
object : SimpleModule() {
init { addKeyDeserializer(Wrapped::class.java, Wrapped.KeyDeserializer()) }
}
)
.build()

val result = mapper.readValue<Map<Wrapper, String?>>("""{"foo-bar":null}""")
val expected = mapOf(Wrapper(Wrapped("foo", "bar")) to null)

assertEquals(expected, result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey.keyDeserializer

import tools.jackson.module.kotlin.readValue
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NonNullObject
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.NullableObject
import tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.Primitive
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import tools.jackson.databind.module.SimpleModule
import tools.jackson.module.kotlin.jacksonMapperBuilder

class SpecifiedForObjectMapperTest {
companion object {
val mapper = jacksonMapperBuilder().apply {
val module = SimpleModule().apply {
this.addKeyDeserializer(Primitive::class.java, Primitive.KeyDeserializer())
this.addKeyDeserializer(NonNullObject::class.java, NonNullObject.KeyDeserializer())
this.addKeyDeserializer(NullableObject::class.java, NullableObject.KeyDeserializer())
}
this.addModule(module)
}.build()
}

@Nested
inner class DirectDeserialize {
@Test
fun primitive() {
val result = mapper.readValue<Map<Primitive, String?>>("""{"1":null}""")
assertEquals(mapOf(Primitive(101) to null), result)
}

@Test
fun nonNullObject() {
val result = mapper.readValue<Map<NonNullObject, String?>>("""{"foo":null}""")
assertEquals(mapOf(NonNullObject("foo-deser") to null), result)
}

@Test
fun nullableObject() {
val result = mapper.readValue<Map<NullableObject, String?>>("""{"bar":null}""")
assertEquals(mapOf(NullableObject("bar-deser") to null), result)
}
}

data class Dst(
val p: Map<Primitive, String?>,
val nn: Map<NonNullObject, String?>,
val n: Map<NullableObject, String?>
)

@Test
fun wrapped() {
val src = """
{
"p":{"1":null},
"nn":{"foo":null},
"n":{"bar":null}
}
""".trimIndent()
val result = mapper.readValue<Dst>(src)
val expected = Dst(
mapOf(Primitive(101) to null),
mapOf(NonNullObject("foo-deser") to null),
mapOf(NullableObject("bar-deser") to null)
)

assertEquals(expected, result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package tools.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey.keyDeserializer.byAnnotation

import tools.jackson.databind.DeserializationContext
import tools.jackson.module.kotlin.defaultMapper
import tools.jackson.module.kotlin.jacksonObjectMapper
import tools.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import tools.jackson.databind.annotation.JsonDeserialize
import tools.jackson.databind.KeyDeserializer as JacksonKeyDeserializer

class SpecifiedForClassTest {
@JsonDeserialize(keyUsing = Value.KeyDeserializer::class)
@JvmInline
value class Value(val v: Int) {
class KeyDeserializer : JacksonKeyDeserializer() {
override fun deserializeKey(key: String, ctxt: DeserializationContext) = Value(key.toInt() + 100)
}
}

@Test
fun directDeserTest() {
val result = defaultMapper.readValue<Map<Value, String?>>("""{"1":null}""")

assertEquals(mapOf(Value(101) to null), result)
}

data class Wrapper(val v: Map<Value, String?>)

@Test
fun paramDeserTest() {
val mapper = jacksonObjectMapper()
val result = mapper.readValue<Wrapper>("""{"v":{"1":null}}""")

assertEquals(Wrapper(mapOf(Value(101) to null)), result)
}
}
Loading

0 comments on commit 3b7adfa

Please sign in to comment.