diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 9af9b8ef..7b07d922 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -15,7 +15,12 @@ Authors: Contributors: -# 2.20.0 (not yet released) +# 2.20.1 (not yet released) + +WrongWrong (@k163377) +* #1057: Fixed a regression related to deserializing value classes with private constructor + +# 2.20.0 (28-Aug-2025) WrongWrong (@k163377) * #1025: Deprecate MissingKotlinParameterException and replace with new exception diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index e6f1b2e9..05fd423c 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -16,6 +16,10 @@ Co-maintainers: === Releases === ------------------------------------------------------------------------ +2.20.1 (not yet released) + +#1057: The issue where deserialization of value classes using private constructor failed starting from version 2.20.0 has been fixed. + 2.20.0 (28-Aug-2025) #1025: When a null is entered for a non-null parameter, the KotlinInvalidNullException is now thrown instead of the 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 7902b072..c8cb4eda 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt @@ -165,7 +165,7 @@ internal class IntValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxedType: Type get() = Int::class.java - override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_INT_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_INT_METHOD_TYPE) override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int } @@ -175,7 +175,7 @@ internal class LongValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxedType: Type get() = Long::class.java - override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_LONG_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_LONG_METHOD_TYPE) override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long } @@ -185,7 +185,7 @@ internal class StringValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxedType: Type get() = String::class.java - override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_STRING_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_STRING_METHOD_TYPE) override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String? } @@ -195,7 +195,7 @@ internal class JavaUuidValueClassUnboxConverter( unboxMethod: Method, ) : ValueClassUnboxConverter() { override val unboxedType: Type get() = UUID::class.java - override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE) override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID? } @@ -205,7 +205,7 @@ internal class GenericValueClassUnboxConverter( override val unboxedType: Type, unboxMethod: Method, ) : ValueClassUnboxConverter() { - override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE) + override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(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 5d49ef11..026669df 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt @@ -2,6 +2,7 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.databind.JsonMappingException +import com.fasterxml.jackson.databind.util.ClassUtil import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType @@ -61,5 +62,10 @@ internal val LONG_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.j 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) +internal fun unreflectWithAccessibilityModification(method: Method): MethodHandle = MethodHandles.lookup().unreflect( + method.apply { ClassUtil.checkAndFixAccess(this, false) }, +) +internal fun unreflectAsTypeWithAccessibilityModification( + method: Method, + type: MethodType, +): MethodHandle = unreflectWithAccessibilityModification(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 9ece0380..2f27acbe 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt @@ -109,7 +109,7 @@ internal sealed class NoConversionCreatorBoxDeserializer( ) : WrapsNullableValueClassDeserializer(converter.boxedClass) { protected abstract val inputType: Class<*> protected val handle: MethodHandle = MethodHandles - .filterReturnValue(unreflect(creator), converter.boxHandle) + .filterReturnValue(unreflectWithAccessibilityModification(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 @@ -190,7 +190,7 @@ internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer( private val handle: MethodHandle init { - val unreflect = unreflect(creator).run { + val unreflect = unreflectWithAccessibilityModification(creator).run { asType(type().changeParameterType(0, Any::class.java)) } handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) @@ -222,7 +222,7 @@ internal class WrapsAnyValueClassBoxDeserializer( private val handle: MethodHandle init { - val unreflect = unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE) + val unreflect = unreflectAsTypeWithAccessibilityModification(creator, ANY_TO_ANY_METHOD_TYPE) handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } 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 0879f423..a06eef1c 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt @@ -112,7 +112,7 @@ internal sealed class ValueClassKeyDeserializer( // 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), + unreflectWithAccessibilityModification(creator), ) internal class WrapsInt( @@ -160,7 +160,7 @@ internal sealed class ValueClassKeyDeserializer( creator: Method, ) : ValueClassKeyDeserializer( converter, - unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE), + unreflectAsTypeWithAccessibilityModification(creator, ANY_TO_ANY_METHOD_TYPE), ) { override val unboxedClass: Class<*> = creator.returnType 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 d6e0b10e..820ee90b 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt @@ -37,7 +37,7 @@ internal sealed class ValueClassStaticJsonKeySerializer( methodType: MethodType, ) : StdSerializer(converter.valueClass) { private val keyType: Class<*> = staticJsonValueGetter.returnType - private val handle: MethodHandle = unreflectAsType(staticJsonValueGetter, methodType).let { + private val handle: MethodHandle = unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, methodType).let { MethodHandles.filterReturnValue(converter.unboxHandle, it) } 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 42aabc2f..db2f15a7 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt @@ -82,7 +82,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - unreflectAsType(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE), + unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE), ) internal class WrapsLong( @@ -90,7 +90,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - unreflectAsType(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE), + unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE), ) internal class WrapsString( @@ -98,7 +98,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - unreflectAsType(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE), + unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE), ) internal class WrapsJavaUuid( @@ -106,7 +106,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - unreflectAsType(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE), + unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE), ) internal class WrapsAny( @@ -114,7 +114,7 @@ internal sealed class ValueClassStaticJsonValueSerializer( staticJsonValueGetter: Method, ) : ValueClassStaticJsonValueSerializer( converter, - unreflectAsType(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE), + unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE), ) companion object { diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/PrivateConstructorTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/PrivateConstructorTest.kt new file mode 100644 index 00000000..7a93ab65 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/PrivateConstructorTest.kt @@ -0,0 +1,76 @@ +package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass + +import com.fasterxml.jackson.module.kotlin.defaultMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class PrivateConstructorTest { + @JvmInline + value class Primitive private constructor(val v: Int) + + @JvmInline + value class NonNullObject private constructor(val v: String) + + @JvmInline + value class NullableObject private constructor(val v: String?) + + @JvmInline + value class NullablePrimitive private constructor(val v: Int?) + + @JvmInline + value class TwoUnitPrimitive private constructor(val v: Long) + + @Nested + inner class DirectDeserializeTest { + @Test + fun primitiveTest() { + val result = defaultMapper.readValue("1") + assertEquals(1, result.v) + } + + @Test + fun nonNullObjectTest() { + val result = defaultMapper.readValue(""""foo"""") + assertEquals("foo", result.v) + } + + @Test + fun nullableObjectTest() { + val result = defaultMapper.readValue(""""bar"""") + assertEquals("bar", result.v) + } + + @Test + fun nullablePrimitiveTest() { + val result = defaultMapper.readValue("2") + assertEquals(2, result.v) + } + + @Test + fun twoUnitPrimitiveTest() { + val result = defaultMapper.readValue("3") + assertEquals(3L, result.v) + } + } + + data class Dto( + val primitive: Primitive, + val nonNullObject: NonNullObject, + val nullableObject: NullableObject, + val nullablePrimitive: NullablePrimitive, + val twoUnitPrimitive: TwoUnitPrimitive, + ) + + @Test + fun wrappedDeserializeTest() { + val src = """{"primitive":1,"nonNullObject":"foo","nullableObject":"bar","nullablePrimitive":2,"twoUnitPrimitive":3}""" + val result = defaultMapper.readValue(src) + assertEquals(1, result.primitive.v) + assertEquals("foo", result.nonNullObject.v) + assertEquals("bar", result.nullableObject.v) + assertEquals(2, result.nullablePrimitive.v) + assertEquals(3L, result.twoUnitPrimitive.v) + } +} diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/PrivateConstructorTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/PrivateConstructorTest.kt new file mode 100644 index 00000000..68357c13 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/kogeraIntegration/deser/valueClass/mapKey/PrivateConstructorTest.kt @@ -0,0 +1,84 @@ +package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey + +import com.fasterxml.jackson.module.kotlin.defaultMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class PrivateConstructorTest { + @JvmInline + value class Primitive private constructor(val v: Int) + + @JvmInline + value class NonNullObject private constructor(val v: String) + + @JvmInline + value class NullableObject private constructor(val v: String?) + + @JvmInline + value class NullablePrimitive private constructor(val v: Int?) + + @JvmInline + value class TwoUnitPrimitive private constructor(val v: Long) + + @Nested + inner class DirectDeserialize { + @Test + fun primitive() { + val result = defaultMapper.readValue>("""{"1":null}""") + assertEquals(1, result.keys.first().v) + } + + @Test + fun nonNullObject() { + val result = defaultMapper.readValue>("""{"foo":null}""") + assertEquals("foo", result.keys.first().v) + } + + @Test + fun nullableObject() { + val result = defaultMapper.readValue>("""{"bar":null}""") + assertEquals("bar", result.keys.first().v) + } + + @Test + fun nullablePrimitive() { + val result = defaultMapper.readValue>("""{"2":null}""") + assertEquals(2, result.keys.first().v) + } + + @Test + fun twoUnitPrimitive() { + val result = defaultMapper.readValue>("""{"1":null}""") + assertEquals(1L, result.keys.first().v) + } + } + + data class Dst( + val p: Map, + val nn: Map, + val n: Map, + val np: Map, + val tup: Map, + ) + + @Test + fun wrapped() { + val src = """ + { + "p":{"1":null}, + "nn":{"foo":null}, + "n":{"bar":null}, + "np":{"2":null}, + "tup":{"2":null} + } + """.trimIndent() + val result = defaultMapper.readValue(src) + assertEquals(1, result.p.keys.first().v) + assertEquals("foo", result.nn.keys.first().v) + assertEquals("bar", result.n.keys.first().v) + assertEquals(2, result.np.keys.first().v) + assertEquals(2L, result.tup.keys.first().v) + } +} diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/GitHub1050.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/GitHub1050.kt new file mode 100644 index 00000000..ba52c3a3 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/GitHub1050.kt @@ -0,0 +1,24 @@ +package com.fasterxml.jackson.module.kotlin.test.github + +import com.fasterxml.jackson.module.kotlin.defaultMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +class GitHub1050Test { + @JvmInline + value class PortCode private constructor(val value: String) : Comparable { + override fun compareTo(other: PortCode): Int { TODO("Not yet implemented") } + + companion object { + operator fun invoke(value: String) = PortCode(value.uppercase(Locale.getDefault())) + } + } + + @Test + fun test() { + val result = defaultMapper.readValue("\"ABC\"") + assertEquals("ABC", result.value) + } +}