diff --git a/pom.xml b/pom.xml
index 384c965d..a822d5e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -255,6 +255,22 @@
com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException#MissingKotlinParameterException(kotlin.reflect.KParameter,java.io.Closeable,java.lang.String)
+
+ com.fasterxml.jackson.module.kotlin.WrapsNullableValueClassBoxDeserializer
+
+ com.fasterxml.jackson.module.kotlin.ValueClassUnboxKeySerializer
+ com.fasterxml.jackson.module.kotlin.KotlinKeySerializersKt
+ com.fasterxml.jackson.module.kotlin.ValueClassSerializer
+
+
+ com.fasterxml.jackson.module.kotlin.KotlinKeySerializers#KotlinKeySerializers()
+
+
+ com.fasterxml.jackson.module.kotlin.KotlinSerializers#KotlinSerializers()
+
+ com.fasterxml.jackson.module.kotlin.ValueClassStaticJsonKeySerializer
+ com.fasterxml.jackson.module.kotlin.ValueClassBoxConverter
+ com.fasterxml.jackson.module.kotlin.ValueClassKeyDeserializer
diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x
index 8e07fc18..0572b9a4 100644
--- a/release-notes/CREDITS-2.x
+++ b/release-notes/CREDITS-2.x
@@ -18,6 +18,7 @@ Contributors:
# 2.20.0 (not yet released)
WrongWrong (@k163377)
+* #1018: Use MethodHandle in processing related to value class
* #969: Cleanup of deprecated contents
* #967: Update settings for 2.20
diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 522ac1bf..d673f62a 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -17,7 +17,12 @@ Co-maintainers:
------------------------------------------------------------------------
2.20.0 (not yet released)
-
+#1018: Improved handling of `value class` has improved performance for both serialization and deserialization.
+ In particular, for serialization, proper caching has improved throughput by a factor of 2 or more in the general cases.
+ Also, replacing function execution by reflection with `MethodHandle` improved throughput by several percent for both serialization and deserialization.
+ In cases where the number of properties of a `value class` in the processing target is large, there is a possibility to obtain a larger improvement.
+ Please note that this modification causes a destructive change in that exceptions thrown during deserialization of
+ `value class` are no longer wrapped in an `InvocationTargetException`.
#969: Deprecated content has been cleaned up with the version upgrade.
#967: Kotlin has been upgraded to 2.0.21.
- Generate SBOMs [JSTEP-14]
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt
index c284fb8c..7902b072 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt
@@ -4,9 +4,13 @@ import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
import com.fasterxml.jackson.databind.type.TypeFactory
-import com.fasterxml.jackson.databind.util.ClassUtil
import com.fasterxml.jackson.databind.util.StdConverter
-import kotlin.reflect.KClass
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
+import java.lang.reflect.Method
+import java.lang.reflect.Type
+import java.util.UUID
import kotlin.time.toJavaDuration
import kotlin.time.toKotlinDuration
import java.time.Duration as JavaDuration
@@ -23,7 +27,7 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon
}
internal object KotlinDurationValueToJavaDurationConverter : StdConverter() {
- private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KotlinDuration::class) }
+ private val boxConverter by lazy { LongValueClassBoxConverter(KotlinDuration::class.java) }
override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value))
}
@@ -45,18 +49,163 @@ internal object JavaToKotlinDurationConverter : StdConverter(
- unboxedClass: Class,
- val boxedClass: KClass
-) : StdConverter() {
- private val boxMethod = boxedClass.java.getDeclaredMethod("box-impl", unboxedClass).apply {
- ClassUtil.checkAndFixAccess(this, false)
+internal sealed class ValueClassBoxConverter : StdConverter() {
+ abstract val boxedClass: Class
+ abstract val boxHandle: MethodHandle
+
+ protected fun rawBoxHandle(
+ unboxedClass: Class<*>,
+ ): MethodHandle = MethodHandles.lookup().findStatic(
+ boxedClass,
+ "box-impl",
+ MethodType.methodType(boxedClass, unboxedClass),
+ )
+
+ val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
+
+ companion object {
+ fun create(
+ unboxedClass: Class<*>,
+ valueClass: Class<*>,
+ ): ValueClassBoxConverter<*, *> = when (unboxedClass) {
+ Int::class.java -> IntValueClassBoxConverter(valueClass)
+ Long::class.java -> LongValueClassBoxConverter(valueClass)
+ String::class.java -> StringValueClassBoxConverter(valueClass)
+ UUID::class.java -> JavaUuidValueClassBoxConverter(valueClass)
+ else -> GenericValueClassBoxConverter(unboxedClass, valueClass)
+ }
}
+ // If the wrapped type is explicitly specified, it is inherited for the sake of distinction
+ internal sealed class Specified : ValueClassBoxConverter()
+}
+
+// region: Converters for common classes as wrapped values, add as needed.
+internal class IntValueClassBoxConverter(
+ override val boxedClass: Class,
+) : ValueClassBoxConverter.Specified() {
+ override val boxHandle: MethodHandle = rawBoxHandle(Int::class.java).asType(INT_TO_ANY_METHOD_TYPE)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun convert(value: Int): D = boxHandle.invokeExact(value) as D
+}
+
+internal class LongValueClassBoxConverter(
+ override val boxedClass: Class,
+) : ValueClassBoxConverter.Specified() {
+ override val boxHandle: MethodHandle = rawBoxHandle(Long::class.java).asType(LONG_TO_ANY_METHOD_TYPE)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun convert(value: Long): D = boxHandle.invokeExact(value) as D
+}
+
+internal class StringValueClassBoxConverter(
+ override val boxedClass: Class,
+) : ValueClassBoxConverter.Specified() {
+ override val boxHandle: MethodHandle = rawBoxHandle(String::class.java).asType(STRING_TO_ANY_METHOD_TYPE)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun convert(value: String?): D = boxHandle.invokeExact(value) as D
+}
+
+internal class JavaUuidValueClassBoxConverter(
+ override val boxedClass: Class,
+) : ValueClassBoxConverter.Specified() {
+ override val boxHandle: MethodHandle = rawBoxHandle(UUID::class.java).asType(JAVA_UUID_TO_ANY_METHOD_TYPE)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun convert(value: UUID?): D = boxHandle.invokeExact(value) as D
+}
+// endregion
+
+/**
+ * A converter that only performs box processing for the value class.
+ * Note that constructor-impl is not called.
+ * @param S is nullable because value corresponds to a nullable value class.
+ * see [io.github.projectmapk.jackson.module.kogera.annotationIntrospector.KotlinFallbackAnnotationIntrospector.findNullSerializer]
+ */
+internal class GenericValueClassBoxConverter(
+ unboxedClass: Class,
+ override val boxedClass: Class,
+) : ValueClassBoxConverter() {
+ override val boxHandle: MethodHandle = rawBoxHandle(unboxedClass).asType(ANY_TO_ANY_METHOD_TYPE)
+
@Suppress("UNCHECKED_CAST")
- override fun convert(value: S): D = boxMethod.invoke(null, value) as D
+ override fun convert(value: S): D = boxHandle.invokeExact(value) as D
+}
+
+internal sealed class ValueClassUnboxConverter : StdConverter() {
+ abstract val valueClass: Class
+ abstract val unboxedType: Type
+ abstract val unboxHandle: MethodHandle
+
+ final override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass)
+ final override fun getOutputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(unboxedType)
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
+
+ companion object {
+ fun create(valueClass: Class<*>): ValueClassUnboxConverter<*, *> {
+ val unboxMethod = valueClass.getDeclaredMethod("unbox-impl")
+ val unboxedType = unboxMethod.genericReturnType
+
+ return when (unboxedType) {
+ Int::class.java -> IntValueClassUnboxConverter(valueClass, unboxMethod)
+ Long::class.java -> LongValueClassUnboxConverter(valueClass, unboxMethod)
+ String::class.java -> StringValueClassUnboxConverter(valueClass, unboxMethod)
+ UUID::class.java -> JavaUuidValueClassUnboxConverter(valueClass, unboxMethod)
+ else -> GenericValueClassUnboxConverter(valueClass, unboxedType, unboxMethod)
+ }
+ }
+ }
+}
+
+internal class IntValueClassUnboxConverter(
+ override val valueClass: Class,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxedType: Type get() = Int::class.java
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_INT_METHOD_TYPE)
+
+ override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int
+}
+
+internal class LongValueClassUnboxConverter(
+ override val valueClass: Class,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxedType: Type get() = Long::class.java
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_LONG_METHOD_TYPE)
+
+ override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long
+}
+
+internal class StringValueClassUnboxConverter(
+ override val valueClass: Class,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxedType: Type get() = String::class.java
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_STRING_METHOD_TYPE)
+
+ override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String?
+}
+
+internal class JavaUuidValueClassUnboxConverter(
+ override val valueClass: Class,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxedType: Type get() = UUID::class.java
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE)
+
+ override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID?
+}
+
+internal class GenericValueClassUnboxConverter(
+ override val valueClass: Class,
+ override val unboxedType: Type,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE)
+
+ override fun convert(value: T): Any? = unboxHandle.invokeExact(value)
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt
index 485ae263..5d49ef11 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt
@@ -2,7 +2,11 @@ package com.fasterxml.jackson.module.kotlin
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.databind.JsonMappingException
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
import java.lang.reflect.AnnotatedElement
+import java.lang.reflect.Method
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KType
@@ -46,3 +50,16 @@ internal fun AnnotatedElement.hasCreatorAnnotation(): Boolean = getAnnotation(Js
// Determine if the unbox result of value class is nullable
internal fun KClass<*>.wrapsNullable(): Boolean =
this.memberProperties.first { it.javaField != null }.returnType.isMarkedNullable
+
+internal val ANY_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Any::class.java) }
+internal val ANY_TO_INT_METHOD_TYPE by lazy {MethodType.methodType(Int::class.java, Any::class.java) }
+internal val ANY_TO_LONG_METHOD_TYPE by lazy {MethodType.methodType(Long::class.java, Any::class.java) }
+internal val ANY_TO_STRING_METHOD_TYPE by lazy {MethodType.methodType(String::class.java, Any::class.java) }
+internal val ANY_TO_JAVA_UUID_METHOD_TYPE by lazy {MethodType.methodType(UUID::class.java, Any::class.java) }
+internal val INT_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Int::class.java) }
+internal val LONG_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Long::class.java) }
+internal val STRING_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, String::class.java) }
+internal val JAVA_UUID_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, UUID::class.java) }
+
+internal fun unreflect(method: Method): MethodHandle = MethodHandles.lookup().unreflect(method)
+internal fun unreflectAsType(method: Method, type: MethodType): MethodHandle = unreflect(method).asType(type)
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt
index 09dd7e02..9ece0380 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt
@@ -11,9 +11,11 @@ import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.deser.Deserializers
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
-import com.fasterxml.jackson.databind.util.ClassUtil
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
import java.lang.reflect.Method
import java.lang.reflect.Modifier
+import java.util.UUID
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaMethod
import kotlin.time.Duration as KotlinDuration
@@ -100,14 +102,128 @@ object ULongDeserializer : StdDeserializer(ULong::class.java) {
)
}
-internal class WrapsNullableValueClassBoxDeserializer(
- private val creator: Method,
- private val converter: ValueClassBoxConverter
+// If the creator does not perform type conversion, implement a unique deserializer for each for fast invocation.
+internal sealed class NoConversionCreatorBoxDeserializer(
+ creator: Method,
+ converter: ValueClassBoxConverter,
) : WrapsNullableValueClassDeserializer(converter.boxedClass) {
- private val inputType: Class<*> = creator.parameterTypes[0]
+ protected abstract val inputType: Class<*>
+ protected val handle: MethodHandle = MethodHandles
+ .filterReturnValue(unreflect(creator), converter.boxHandle)
+
+ // Since the input to handle must be strict, invoke should be implemented in each class
+ protected abstract fun invokeExact(value: S): D
+
+ // Cache the result of wrapping null, since the result is always expected to be the same.
+ @get:JvmName("boxedNullValue")
+ private val boxedNullValue: D by lazy {
+ // For the sake of commonality, it is unavoidably called without checking.
+ // It is controlled by KotlinValueInstantiator, so it is not expected to reach this branch.
+ @Suppress("UNCHECKED_CAST")
+ invokeExact(null as S)
+ }
+
+ final override fun getBoxedNullValue(): D = boxedNullValue
+
+ final override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D {
+ @Suppress("UNCHECKED_CAST")
+ return invokeExact(p.readValueAs(inputType) as S)
+ }
+
+ internal class WrapsInt(
+ creator: Method,
+ converter: IntValueClassBoxConverter,
+ ) : NoConversionCreatorBoxDeserializer(creator, converter) {
+ override val inputType get() = Int::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: Int): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsLong(
+ creator: Method,
+ converter: LongValueClassBoxConverter,
+ ) : NoConversionCreatorBoxDeserializer(creator, converter) {
+ override val inputType get() = Long::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: Long): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsString(
+ creator: Method,
+ converter: StringValueClassBoxConverter,
+ ) : NoConversionCreatorBoxDeserializer(creator, converter) {
+ override val inputType get() = String::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: String?): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsJavaUuid(
+ creator: Method,
+ converter: JavaUuidValueClassBoxConverter,
+ ) : NoConversionCreatorBoxDeserializer(creator, converter) {
+ override val inputType get() = UUID::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D
+ }
+
+ companion object {
+ fun create(creator: Method, converter: ValueClassBoxConverter.Specified) = when (converter) {
+ is IntValueClassBoxConverter -> WrapsInt(creator, converter)
+ is LongValueClassBoxConverter -> WrapsLong(creator, converter)
+ is StringValueClassBoxConverter -> WrapsString(creator, converter)
+ is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(creator, converter)
+ }
+ }
+}
+
+// Even if the creator performs type conversion, it is distinguished
+// because a speedup due to rtype matching of filterReturnValue can be expected for the specified type.
+internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer(
+ creator: Method,
+ private val inputType: Class<*>,
+ converter: ValueClassBoxConverter,
+) : WrapsNullableValueClassDeserializer(converter.boxedClass) {
+ private val handle: MethodHandle
init {
- ClassUtil.checkAndFixAccess(creator, false)
+ val unreflect = unreflect(creator).run {
+ asType(type().changeParameterType(0, Any::class.java))
+ }
+ handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle)
+ }
+
+ // Cache the result of wrapping null, since the result is always expected to be the same.
+ @get:JvmName("boxedNullValue")
+ private val boxedNullValue: D by lazy { instantiate(null) }
+
+ override fun getBoxedNullValue(): D = boxedNullValue
+
+ // To instantiate the value class in the same way as other classes,
+ // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order.
+ // Input is null only when called from KotlinValueInstantiator.
+ @Suppress("UNCHECKED_CAST")
+ private fun instantiate(input: Any?): D = handle.invokeExact(input) as D
+
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D {
+ val input = p.readValueAs(inputType)
+ return instantiate(input)
+ }
+}
+
+internal class WrapsAnyValueClassBoxDeserializer(
+ creator: Method,
+ private val inputType: Class<*>,
+ converter: GenericValueClassBoxConverter,
+) : WrapsNullableValueClassDeserializer(converter.boxedClass) {
+ private val handle: MethodHandle
+
+ init {
+ val unreflect = unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE)
+ handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle)
}
// Cache the result of wrapping null, since the result is always expected to be the same.
@@ -120,7 +236,7 @@ internal class WrapsNullableValueClassBoxDeserializer(
// it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order.
// Input is null only when called from KotlinValueInstantiator.
@Suppress("UNCHECKED_CAST")
- private fun instantiate(input: Any?): D = converter.convert(creator.invoke(null, input) as S)
+ private fun instantiate(input: Any?): D = handle.invokeExact(input) as D
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D {
val input = p.readValueAs(inputType)
@@ -172,8 +288,21 @@ internal class KotlinDeserializers(
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass)?.let {
val unboxedClass = it.returnType
- val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass.kotlin)
- WrapsNullableValueClassBoxDeserializer(it, converter)
+ val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass)
+
+ when (converter) {
+ is ValueClassBoxConverter.Specified -> {
+ val inputType = it.parameterTypes[0]
+
+ if (inputType == unboxedClass) {
+ NoConversionCreatorBoxDeserializer.create(it, converter)
+ } else {
+ HasConversionCreatorWrapsSpecifiedBoxDeserializer(it, inputType, converter)
+ }
+ }
+ is GenericValueClassBoxConverter ->
+ WrapsAnyValueClassBoxDeserializer(it, it.parameterTypes[0], converter)
+ }
}
else -> null
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt
index 8658747a..0879f423 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt
@@ -6,8 +6,10 @@ import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializer
import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializers
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
-import com.fasterxml.jackson.databind.util.ClassUtil
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
import java.lang.reflect.Method
+import java.util.UUID
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaMethod
@@ -72,34 +74,100 @@ internal object ULongKeyDeserializer : StdKeyDeserializer(TYPE_LONG, ULong::clas
}
// The implementation is designed to be compatible with various creators, just in case.
-internal class ValueClassKeyDeserializer(
- private val creator: Method,
- private val converter: ValueClassBoxConverter
+internal sealed class ValueClassKeyDeserializer(
+ converter: ValueClassBoxConverter,
+ creatorHandle: MethodHandle,
) : KeyDeserializer() {
- private val unboxedClass: Class<*> = creator.parameterTypes[0]
+ private val boxedClass: Class = converter.boxedClass
- init {
- ClassUtil.checkAndFixAccess(creator, false)
- }
+ protected abstract val unboxedClass: Class<*>
+ protected val handle: MethodHandle = MethodHandles.filterReturnValue(creatorHandle, converter.boxHandle)
// 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"
+ private fun errorMessage(boxedType: JavaType): String = "Could not find (Map) Key deserializer for types " +
+ "wrapped in $boxedType"
+
+ // Since the input to handle must be strict, invoke should be implemented in each class
+ protected abstract fun invokeExact(value: S): D
- override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any {
+ final 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)
+ invokeExact(value as S)
} catch (e: InvalidDefinitionException) {
- throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass.java)), e)
+ throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(boxedClass)), e)
}
}
+ internal sealed class WrapsSpecified(
+ converter: ValueClassBoxConverter,
+ creator: Method,
+ ) : ValueClassKeyDeserializer(
+ converter,
+ // Currently, only the primary constructor can be the creator of a key, so for specified types,
+ // the return type of the primary constructor and the input type of the box function are exactly the same.
+ // Therefore, performance is improved by omitting the asType call.
+ unreflect(creator),
+ )
+
+ internal class WrapsInt(
+ converter: IntValueClassBoxConverter,
+ creator: Method,
+ ) : WrapsSpecified(converter, creator) {
+ override val unboxedClass: Class<*> get() = Int::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: Int): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsLong(
+ converter: LongValueClassBoxConverter,
+ creator: Method,
+ ) : WrapsSpecified(converter, creator) {
+ override val unboxedClass: Class<*> get() = Long::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: Long): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsString(
+ converter: StringValueClassBoxConverter,
+ creator: Method,
+ ) : WrapsSpecified(converter, creator) {
+ override val unboxedClass: Class<*> get() = String::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: String?): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsJavaUuid(
+ converter: JavaUuidValueClassBoxConverter,
+ creator: Method,
+ ) : WrapsSpecified(converter, creator) {
+ override val unboxedClass: Class<*> get() = UUID::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsAny(
+ converter: GenericValueClassBoxConverter,
+ creator: Method,
+ ) : ValueClassKeyDeserializer(
+ converter,
+ unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE),
+ ) {
+ override val unboxedClass: Class<*> = creator.returnType
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: S): D = handle.invokeExact(value) as D
+ }
+
companion object {
fun createOrNull(
boxedClass: KClass<*>,
@@ -113,7 +181,13 @@ internal class ValueClassKeyDeserializer(
val creator = boxedClass.primaryConstructor?.javaMethod ?: return null
val converter = cache.getValueClassBoxConverter(creator.returnType, boxedClass)
- return ValueClassKeyDeserializer(creator, converter)
+ return when (converter) {
+ is IntValueClassBoxConverter -> WrapsInt(converter, creator)
+ is LongValueClassBoxConverter -> WrapsLong(converter, creator)
+ is StringValueClassBoxConverter -> WrapsString(converter, creator)
+ is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(converter, creator)
+ is GenericValueClassBoxConverter -> WrapsAny(converter, creator)
+ }
}
}
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt
index c6a5d774..d6e0b10e 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt
@@ -9,18 +9,20 @@ import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.Serializers
import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
import java.lang.reflect.Method
import java.lang.reflect.Modifier
-internal object ValueClassUnboxKeySerializer : StdSerializer(Any::class.java) {
- private fun readResolve(): Any = ValueClassUnboxKeySerializer
-
- override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
- val method = value::class.java.getMethod("unbox-impl")
- val unboxed = method.invoke(value)
+internal class ValueClassUnboxKeySerializer(
+ private val converter: ValueClassUnboxConverter,
+) : StdSerializer(converter.valueClass) {
+ override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
+ val unboxed = converter.convert(value)
if (unboxed == null) {
- val javaType = provider.typeFactory.constructType(method.genericReturnType)
+ val javaType = converter.getOutputType(provider.typeFactory)
provider.findNullKeySerializer(javaType, null).serialize(null, gen, provider)
return
}
@@ -29,21 +31,18 @@ internal object ValueClassUnboxKeySerializer : StdSerializer(Any::class.jav
}
}
-// Class must be UnboxableValueClass.
-private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method ->
- Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value }
-}
-
-internal class ValueClassStaticJsonKeySerializer(
- t: Class,
- private val staticJsonKeyGetter: Method
-) : StdSerializer(t) {
- private val keyType: Class<*> = staticJsonKeyGetter.returnType
- private val unboxMethod: Method = t.getMethod("unbox-impl")
+internal sealed class ValueClassStaticJsonKeySerializer(
+ converter: ValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ methodType: MethodType,
+) : StdSerializer(converter.valueClass) {
+ private val keyType: Class<*> = staticJsonValueGetter.returnType
+ private val handle: MethodHandle = unreflectAsType(staticJsonValueGetter, methodType).let {
+ MethodHandles.filterReturnValue(converter.unboxHandle, it)
+ }
- override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
- val unboxed = unboxMethod.invoke(value)
- val jsonKey: Any? = staticJsonKeyGetter.invoke(null, unboxed)
+ final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
+ val jsonKey: Any? = handle.invokeExact(value)
val serializer = jsonKey
?.let { provider.findKeySerializer(keyType, null) }
@@ -52,20 +51,94 @@ internal class ValueClassStaticJsonKeySerializer(
serializer.serialize(jsonKey, gen, provider)
}
+ internal class WrapsInt(
+ converter: IntValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ INT_TO_ANY_METHOD_TYPE,
+ )
+
+ internal class WrapsLong(
+ converter: LongValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ LONG_TO_ANY_METHOD_TYPE,
+ )
+
+ internal class WrapsString(
+ converter: StringValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ STRING_TO_ANY_METHOD_TYPE,
+ )
+
+ internal class WrapsJavaUuid(
+ converter: JavaUuidValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ JAVA_UUID_TO_ANY_METHOD_TYPE,
+ )
+
+ internal class WrapsAny(
+ converter: GenericValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ ANY_TO_ANY_METHOD_TYPE,
+
+ )
+
companion object {
- fun createOrNull(t: Class<*>): StdSerializer<*>? =
- t.getStaticJsonKeyGetter()?.let { ValueClassStaticJsonKeySerializer(t, it) }
+ // Class must be UnboxableValueClass.
+ private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method ->
+ Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value }
+ }
+
+ // `t` must be UnboxableValueClass.
+ // If create a function with a JsonValue in the value class,
+ // it will be compiled as a static method (= cannot be processed properly by Jackson),
+ // so use a ValueClassSerializer.StaticJsonValue to handle this.
+ fun createOrNull(
+ converter: ValueClassUnboxConverter,
+ ): ValueClassStaticJsonKeySerializer? = converter
+ .valueClass
+ .getStaticJsonKeyGetter()
+ ?.let {
+ when (converter) {
+ is IntValueClassUnboxConverter -> WrapsInt(converter, it)
+ is LongValueClassUnboxConverter -> WrapsLong(converter, it)
+ is StringValueClassUnboxConverter -> WrapsString(converter, it)
+ is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it)
+ is GenericValueClassUnboxConverter -> WrapsAny(converter, it)
+ }
+ }
}
}
-internal class KotlinKeySerializers : Serializers.Base() {
+internal class KotlinKeySerializers(private val cache: ReflectionCache) : Serializers.Base() {
override fun findSerializer(
config: SerializationConfig,
type: JavaType,
- beanDesc: BeanDescription
- ): JsonSerializer<*>? = when {
- type.rawClass.isUnboxableValueClass() -> ValueClassStaticJsonKeySerializer.createOrNull(type.rawClass)
- ?: ValueClassUnboxKeySerializer
- else -> null
+ beanDesc: BeanDescription,
+ ): JsonSerializer<*>? {
+ val rawClass = type.rawClass
+
+ return when {
+ rawClass.isUnboxableValueClass() -> {
+ val unboxConverter = cache.getValueClassUnboxConverter(rawClass)
+ ValueClassStaticJsonKeySerializer.createOrNull(unboxConverter)
+ ?: ValueClassUnboxKeySerializer(unboxConverter)
+ }
+ else -> null
+ }
}
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt
index 397d93a4..d23afd4e 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt
@@ -133,8 +133,8 @@ class KotlinModule private constructor(
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
context.addKeyDeserializers(KotlinKeyDeserializers(cache))
- context.addSerializers(KotlinSerializers())
- context.addKeySerializers(KotlinKeySerializers())
+ context.addSerializers(KotlinSerializers(cache))
+ context.addKeySerializers(KotlinKeySerializers(cache))
// ranges
context.setMixInAnnotations(ClosedRange::class.java, ClosedRangeMixin::class.java)
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt
index 82bcb164..42aabc2f 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt
@@ -9,6 +9,8 @@ import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.Serializers
import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.math.BigInteger
@@ -46,11 +48,10 @@ object ULongSerializer : StdSerializer(ULong::class.java) {
}
}
-// Class must be UnboxableValueClass.
-private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods.find { method ->
- Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value }
-}
-
+@Deprecated(
+ message = "This class was published by mistake. It will be removed in `2.22.0` as it is no longer used internally.",
+ level = DeprecationLevel.WARNING
+)
object ValueClassUnboxSerializer : StdSerializer(Any::class.java) {
private fun readResolve(): Any = ValueClassUnboxSerializer
@@ -60,32 +61,85 @@ object ValueClassUnboxSerializer : StdSerializer(Any::class.java) {
}
}
-internal sealed class ValueClassSerializer(t: Class) : StdSerializer(t) {
- class StaticJsonValue(
- t: Class, private val staticJsonValueGetter: Method
- ) : ValueClassSerializer(t) {
- private val unboxMethod: Method = t.getMethod("unbox-impl")
-
- override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
- val unboxed = unboxMethod.invoke(value)
- // As shown in the processing of the factory function, jsonValueGetter is always a static method.
- val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed)
- provider.defaultSerializeValue(jsonValue, gen)
- }
+// Class must be UnboxableValueClass.
+private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods.find { method ->
+ Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value }
+}
+
+internal sealed class ValueClassStaticJsonValueSerializer(
+ converter: ValueClassUnboxConverter,
+ staticJsonValueHandle: MethodHandle,
+) : StdSerializer(converter.valueClass) {
+ private val handle: MethodHandle = MethodHandles.filterReturnValue(converter.unboxHandle, staticJsonValueHandle)
+
+ final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
+ val jsonValue: Any? = handle.invokeExact(value)
+ provider.defaultSerializeValue(jsonValue, gen)
}
+ internal class WrapsInt(
+ converter: IntValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE),
+ )
+
+ internal class WrapsLong(
+ converter: LongValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE),
+ )
+
+ internal class WrapsString(
+ converter: StringValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE),
+ )
+
+ internal class WrapsJavaUuid(
+ converter: JavaUuidValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE),
+ )
+
+ internal class WrapsAny(
+ converter: GenericValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE),
+ )
+
companion object {
// `t` must be UnboxableValueClass.
// If create a function with a JsonValue in the value class,
// it will be compiled as a static method (= cannot be processed properly by Jackson),
// so use a ValueClassSerializer.StaticJsonValue to handle this.
- fun from(t: Class<*>): StdSerializer<*> = t.getStaticJsonValueGetter()
- ?.let { StaticJsonValue(t, it) }
- ?: ValueClassUnboxSerializer
+ fun createOrNull(
+ converter: ValueClassUnboxConverter,
+ ): ValueClassStaticJsonValueSerializer? = converter
+ .valueClass
+ .getStaticJsonValueGetter()
+ ?.let {
+ when (converter) {
+ is IntValueClassUnboxConverter -> WrapsInt(converter, it)
+ is LongValueClassUnboxConverter -> WrapsLong(converter, it)
+ is StringValueClassUnboxConverter -> WrapsString(converter, it)
+ is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it)
+ is GenericValueClassUnboxConverter -> WrapsAny(converter, it)
+ }
+ }
}
}
-internal class KotlinSerializers : Serializers.Base() {
+internal class KotlinSerializers(private val cache: ReflectionCache) : Serializers.Base() {
override fun findSerializer(
config: SerializationConfig?,
type: JavaType,
@@ -99,7 +153,10 @@ internal class KotlinSerializers : Serializers.Base() {
UInt::class.java == rawClass -> UIntSerializer
ULong::class.java == rawClass -> ULongSerializer
// The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers.
- rawClass.isUnboxableValueClass() -> ValueClassSerializer.from(rawClass)
+ rawClass.isUnboxableValueClass() -> {
+ val unboxConverter = cache.getValueClassUnboxConverter(rawClass)
+ ValueClassStaticJsonValueSerializer.createOrNull(unboxConverter) ?: unboxConverter.delegatingSerializer
+ }
else -> null
}
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt
index 3960deb5..f7762dfc 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt
@@ -12,6 +12,7 @@ import java.lang.reflect.Constructor
import java.lang.reflect.Executable
import java.lang.reflect.Method
import java.util.*
+import kotlin.jvm.java
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
@@ -23,7 +24,7 @@ import kotlin.reflect.jvm.kotlinFunction
internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
companion object {
// Increment is required when properties that use LRUMap are changed.
- private const val serialVersionUID = 4L
+ private const val serialVersionUID = 5L
}
private val javaExecutableToKotlin = LRUMap>(reflectionCacheSize, reflectionCacheSize)
@@ -36,7 +37,9 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
// TODO: Consider whether the cache size should be reduced more,
// since the cache is used only twice locally at initialization per property.
- private val valueClassBoxConverterCache: LRUMap, ValueClassBoxConverter<*, *>> =
+ private val valueClassBoxConverterCache: LRUMap, ValueClassBoxConverter<*, *>> =
+ LRUMap(0, reflectionCacheSize)
+ private val valueClassUnboxConverterCache: LRUMap, ValueClassUnboxConverter<*, *>> =
LRUMap(0, reflectionCacheSize)
// If the Record type defined in Java is processed,
@@ -120,12 +123,21 @@ internal class ReflectionCache(reflectionCacheSize: Int) : Serializable {
}.orElse(null)
}
- fun getValueClassBoxConverter(unboxedClass: Class<*>, boxedClass: KClass<*>): ValueClassBoxConverter<*, *> =
+ fun getValueClassBoxConverter(unboxedClass: Class<*>, boxedClass: Class<*>): ValueClassBoxConverter<*, *> =
valueClassBoxConverterCache.get(boxedClass) ?: run {
- val value = ValueClassBoxConverter(unboxedClass, boxedClass)
+ val value = ValueClassBoxConverter.create(unboxedClass, boxedClass)
(valueClassBoxConverterCache.putIfAbsent(boxedClass, value) ?: value)
}
+ fun getValueClassBoxConverter(unboxedClass: Class<*>, boxedClass: KClass<*>): ValueClassBoxConverter<*, *> =
+ getValueClassBoxConverter(unboxedClass, boxedClass.java)
+
+ fun getValueClassUnboxConverter(boxedClass: Class<*>): ValueClassUnboxConverter<*, *> =
+ valueClassUnboxConverterCache.get(boxedClass) ?: run {
+ val value = ValueClassUnboxConverter.create(boxedClass)
+ (valueClassUnboxConverterCache.putIfAbsent(boxedClass, value) ?: value)
+ }
+
fun findKotlinParameter(param: AnnotatedParameter): KParameter? = when (val owner = param.owner.member) {
is Constructor<*> -> kotlinFromJava(owner)
is Method -> kotlinFromJava(owner)
diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt
index 01db6009..07dde3a6 100644
--- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt
+++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt
@@ -7,7 +7,6 @@ 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 java.lang.reflect.InvocationTargetException
import kotlin.test.assertNotEquals
class WithoutCustomDeserializeMethodTest {
@@ -132,8 +131,8 @@ class WithoutCustomDeserializeMethodTest {
@Test
fun callConstructorCheckTest() {
- val e = assertThrows { defaultMapper.readValue("-1") }
- assertTrue(e.cause === throwable)
+ val e = assertThrows { defaultMapper.readValue("-1") }
+ assertTrue(e === throwable)
}
// If all JsonCreator tests are OK, no need to check throws from factory functions.
diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt
index bf60a014..25029693 100644
--- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt
+++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt
@@ -17,7 +17,6 @@ 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 java.lang.reflect.InvocationTargetException
import com.fasterxml.jackson.databind.KeyDeserializer as JacksonKeyDeserializer
class WithoutCustomDeserializeMethodTest {
@@ -98,10 +97,10 @@ class WithoutCustomDeserializeMethodTest {
@Test
fun callConstructorCheckTest() {
- val e = assertThrows {
+ val e = assertThrows {
defaultMapper.readValue