Skip to content

Commit 9485e6a

Browse files
authored
Merge pull request #1057 from k163377/fix-1050
Fixed a regression related to deserializing value classes with private constructor
2 parents 71d0999 + 9a3a8a2 commit 9485e6a

File tree

11 files changed

+218
-19
lines changed

11 files changed

+218
-19
lines changed

release-notes/CREDITS-2.x

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ Authors:
1515

1616
Contributors:
1717

18-
# 2.20.0 (not yet released)
18+
# 2.20.1 (not yet released)
19+
20+
WrongWrong (@k163377)
21+
* #1057: Fixed a regression related to deserializing value classes with private constructor
22+
23+
# 2.20.0 (28-Aug-2025)
1924

2025
WrongWrong (@k163377)
2126
* #1025: Deprecate MissingKotlinParameterException and replace with new exception

release-notes/VERSION-2.x

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Co-maintainers:
1616
=== Releases ===
1717
------------------------------------------------------------------------
1818

19+
2.20.1 (not yet released)
20+
21+
#1057: The issue where deserialization of value classes using private constructor failed starting from version 2.20.0 has been fixed.
22+
1923
2.20.0 (28-Aug-2025)
2024

2125
#1025: When a null is entered for a non-null parameter, the KotlinInvalidNullException is now thrown instead of the

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ internal class IntValueClassUnboxConverter<T : Any>(
165165
unboxMethod: Method,
166166
) : ValueClassUnboxConverter<T, Int>() {
167167
override val unboxedType: Type get() = Int::class.java
168-
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_INT_METHOD_TYPE)
168+
override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_INT_METHOD_TYPE)
169169

170170
override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int
171171
}
@@ -175,7 +175,7 @@ internal class LongValueClassUnboxConverter<T : Any>(
175175
unboxMethod: Method,
176176
) : ValueClassUnboxConverter<T, Long>() {
177177
override val unboxedType: Type get() = Long::class.java
178-
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_LONG_METHOD_TYPE)
178+
override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_LONG_METHOD_TYPE)
179179

180180
override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long
181181
}
@@ -185,7 +185,7 @@ internal class StringValueClassUnboxConverter<T : Any>(
185185
unboxMethod: Method,
186186
) : ValueClassUnboxConverter<T, String?>() {
187187
override val unboxedType: Type get() = String::class.java
188-
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_STRING_METHOD_TYPE)
188+
override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_STRING_METHOD_TYPE)
189189

190190
override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String?
191191
}
@@ -195,7 +195,7 @@ internal class JavaUuidValueClassUnboxConverter<T : Any>(
195195
unboxMethod: Method,
196196
) : ValueClassUnboxConverter<T, UUID?>() {
197197
override val unboxedType: Type get() = UUID::class.java
198-
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE)
198+
override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE)
199199

200200
override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID?
201201
}
@@ -205,7 +205,7 @@ internal class GenericValueClassUnboxConverter<T : Any>(
205205
override val unboxedType: Type,
206206
unboxMethod: Method,
207207
) : ValueClassUnboxConverter<T, Any?>() {
208-
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE)
208+
override val unboxHandle: MethodHandle = unreflectAsTypeWithAccessibilityModification(unboxMethod, ANY_TO_ANY_METHOD_TYPE)
209209

210210
override fun convert(value: T): Any? = unboxHandle.invokeExact(value)
211211
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.fasterxml.jackson.module.kotlin
22

33
import com.fasterxml.jackson.annotation.JsonCreator
44
import com.fasterxml.jackson.databind.JsonMappingException
5+
import com.fasterxml.jackson.databind.util.ClassUtil
56
import java.lang.invoke.MethodHandle
67
import java.lang.invoke.MethodHandles
78
import java.lang.invoke.MethodType
@@ -61,5 +62,10 @@ internal val LONG_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.j
6162
internal val STRING_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, String::class.java) }
6263
internal val JAVA_UUID_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, UUID::class.java) }
6364

64-
internal fun unreflect(method: Method): MethodHandle = MethodHandles.lookup().unreflect(method)
65-
internal fun unreflectAsType(method: Method, type: MethodType): MethodHandle = unreflect(method).asType(type)
65+
internal fun unreflectWithAccessibilityModification(method: Method): MethodHandle = MethodHandles.lookup().unreflect(
66+
method.apply { ClassUtil.checkAndFixAccess(this, false) },
67+
)
68+
internal fun unreflectAsTypeWithAccessibilityModification(
69+
method: Method,
70+
type: MethodType,
71+
): MethodHandle = unreflectWithAccessibilityModification(method).asType(type)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ internal sealed class NoConversionCreatorBoxDeserializer<S, D : Any>(
109109
) : WrapsNullableValueClassDeserializer<D>(converter.boxedClass) {
110110
protected abstract val inputType: Class<*>
111111
protected val handle: MethodHandle = MethodHandles
112-
.filterReturnValue(unreflect(creator), converter.boxHandle)
112+
.filterReturnValue(unreflectWithAccessibilityModification(creator), converter.boxHandle)
113113

114114
// Since the input to handle must be strict, invoke should be implemented in each class
115115
protected abstract fun invokeExact(value: S): D
@@ -190,7 +190,7 @@ internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer<S, D : Any>(
190190
private val handle: MethodHandle
191191

192192
init {
193-
val unreflect = unreflect(creator).run {
193+
val unreflect = unreflectWithAccessibilityModification(creator).run {
194194
asType(type().changeParameterType(0, Any::class.java))
195195
}
196196
handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle)
@@ -222,7 +222,7 @@ internal class WrapsAnyValueClassBoxDeserializer<S, D : Any>(
222222
private val handle: MethodHandle
223223

224224
init {
225-
val unreflect = unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE)
225+
val unreflect = unreflectAsTypeWithAccessibilityModification(creator, ANY_TO_ANY_METHOD_TYPE)
226226
handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle)
227227
}
228228

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ internal sealed class ValueClassKeyDeserializer<S, D : Any>(
112112
// Currently, only the primary constructor can be the creator of a key, so for specified types,
113113
// the return type of the primary constructor and the input type of the box function are exactly the same.
114114
// Therefore, performance is improved by omitting the asType call.
115-
unreflect(creator),
115+
unreflectWithAccessibilityModification(creator),
116116
)
117117

118118
internal class WrapsInt<D : Any>(
@@ -160,7 +160,7 @@ internal sealed class ValueClassKeyDeserializer<S, D : Any>(
160160
creator: Method,
161161
) : ValueClassKeyDeserializer<S, D>(
162162
converter,
163-
unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE),
163+
unreflectAsTypeWithAccessibilityModification(creator, ANY_TO_ANY_METHOD_TYPE),
164164
) {
165165
override val unboxedClass: Class<*> = creator.returnType
166166

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ internal sealed class ValueClassStaticJsonKeySerializer<T : Any>(
3737
methodType: MethodType,
3838
) : StdSerializer<T>(converter.valueClass) {
3939
private val keyType: Class<*> = staticJsonValueGetter.returnType
40-
private val handle: MethodHandle = unreflectAsType(staticJsonValueGetter, methodType).let {
40+
private val handle: MethodHandle = unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, methodType).let {
4141
MethodHandles.filterReturnValue(converter.unboxHandle, it)
4242
}
4343

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,39 +82,39 @@ internal sealed class ValueClassStaticJsonValueSerializer<T : Any>(
8282
staticJsonValueGetter: Method,
8383
) : ValueClassStaticJsonValueSerializer<T>(
8484
converter,
85-
unreflectAsType(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE),
85+
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE),
8686
)
8787

8888
internal class WrapsLong<T : Any>(
8989
converter: LongValueClassUnboxConverter<T>,
9090
staticJsonValueGetter: Method,
9191
) : ValueClassStaticJsonValueSerializer<T>(
9292
converter,
93-
unreflectAsType(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE),
93+
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE),
9494
)
9595

9696
internal class WrapsString<T : Any>(
9797
converter: StringValueClassUnboxConverter<T>,
9898
staticJsonValueGetter: Method,
9999
) : ValueClassStaticJsonValueSerializer<T>(
100100
converter,
101-
unreflectAsType(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE),
101+
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE),
102102
)
103103

104104
internal class WrapsJavaUuid<T : Any>(
105105
converter: JavaUuidValueClassUnboxConverter<T>,
106106
staticJsonValueGetter: Method,
107107
) : ValueClassStaticJsonValueSerializer<T>(
108108
converter,
109-
unreflectAsType(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE),
109+
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE),
110110
)
111111

112112
internal class WrapsAny<T : Any>(
113113
converter: GenericValueClassUnboxConverter<T>,
114114
staticJsonValueGetter: Method,
115115
) : ValueClassStaticJsonValueSerializer<T>(
116116
converter,
117-
unreflectAsType(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE),
117+
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE),
118118
)
119119

120120
companion object {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass
2+
3+
import com.fasterxml.jackson.module.kotlin.defaultMapper
4+
import com.fasterxml.jackson.module.kotlin.readValue
5+
import org.junit.jupiter.api.Assertions.assertEquals
6+
import org.junit.jupiter.api.Nested
7+
import org.junit.jupiter.api.Test
8+
9+
class PrivateConstructorTest {
10+
@JvmInline
11+
value class Primitive private constructor(val v: Int)
12+
13+
@JvmInline
14+
value class NonNullObject private constructor(val v: String)
15+
16+
@JvmInline
17+
value class NullableObject private constructor(val v: String?)
18+
19+
@JvmInline
20+
value class NullablePrimitive private constructor(val v: Int?)
21+
22+
@JvmInline
23+
value class TwoUnitPrimitive private constructor(val v: Long)
24+
25+
@Nested
26+
inner class DirectDeserializeTest {
27+
@Test
28+
fun primitiveTest() {
29+
val result = defaultMapper.readValue<Primitive>("1")
30+
assertEquals(1, result.v)
31+
}
32+
33+
@Test
34+
fun nonNullObjectTest() {
35+
val result = defaultMapper.readValue<NonNullObject>(""""foo"""")
36+
assertEquals("foo", result.v)
37+
}
38+
39+
@Test
40+
fun nullableObjectTest() {
41+
val result = defaultMapper.readValue<NullableObject>(""""bar"""")
42+
assertEquals("bar", result.v)
43+
}
44+
45+
@Test
46+
fun nullablePrimitiveTest() {
47+
val result = defaultMapper.readValue<NullablePrimitive>("2")
48+
assertEquals(2, result.v)
49+
}
50+
51+
@Test
52+
fun twoUnitPrimitiveTest() {
53+
val result = defaultMapper.readValue<TwoUnitPrimitive>("3")
54+
assertEquals(3L, result.v)
55+
}
56+
}
57+
58+
data class Dto(
59+
val primitive: Primitive,
60+
val nonNullObject: NonNullObject,
61+
val nullableObject: NullableObject,
62+
val nullablePrimitive: NullablePrimitive,
63+
val twoUnitPrimitive: TwoUnitPrimitive,
64+
)
65+
66+
@Test
67+
fun wrappedDeserializeTest() {
68+
val src = """{"primitive":1,"nonNullObject":"foo","nullableObject":"bar","nullablePrimitive":2,"twoUnitPrimitive":3}"""
69+
val result = defaultMapper.readValue<Dto>(src)
70+
assertEquals(1, result.primitive.v)
71+
assertEquals("foo", result.nonNullObject.v)
72+
assertEquals("bar", result.nullableObject.v)
73+
assertEquals(2, result.nullablePrimitive.v)
74+
assertEquals(3L, result.twoUnitPrimitive.v)
75+
}
76+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.fasterxml.jackson.module.kotlin.kogeraIntegration.deser.valueClass.mapKey
2+
3+
import com.fasterxml.jackson.module.kotlin.defaultMapper
4+
import com.fasterxml.jackson.module.kotlin.readValue
5+
import org.junit.jupiter.api.Assertions.assertEquals
6+
import org.junit.jupiter.api.Nested
7+
import org.junit.jupiter.api.Test
8+
9+
class PrivateConstructorTest {
10+
@JvmInline
11+
value class Primitive private constructor(val v: Int)
12+
13+
@JvmInline
14+
value class NonNullObject private constructor(val v: String)
15+
16+
@JvmInline
17+
value class NullableObject private constructor(val v: String?)
18+
19+
@JvmInline
20+
value class NullablePrimitive private constructor(val v: Int?)
21+
22+
@JvmInline
23+
value class TwoUnitPrimitive private constructor(val v: Long)
24+
25+
@Nested
26+
inner class DirectDeserialize {
27+
@Test
28+
fun primitive() {
29+
val result = defaultMapper.readValue<Map<Primitive, String?>>("""{"1":null}""")
30+
assertEquals(1, result.keys.first().v)
31+
}
32+
33+
@Test
34+
fun nonNullObject() {
35+
val result = defaultMapper.readValue<Map<NonNullObject, String?>>("""{"foo":null}""")
36+
assertEquals("foo", result.keys.first().v)
37+
}
38+
39+
@Test
40+
fun nullableObject() {
41+
val result = defaultMapper.readValue<Map<NullableObject, String?>>("""{"bar":null}""")
42+
assertEquals("bar", result.keys.first().v)
43+
}
44+
45+
@Test
46+
fun nullablePrimitive() {
47+
val result = defaultMapper.readValue<Map<NullablePrimitive, String?>>("""{"2":null}""")
48+
assertEquals(2, result.keys.first().v)
49+
}
50+
51+
@Test
52+
fun twoUnitPrimitive() {
53+
val result = defaultMapper.readValue<Map<TwoUnitPrimitive, String?>>("""{"1":null}""")
54+
assertEquals(1L, result.keys.first().v)
55+
}
56+
}
57+
58+
data class Dst(
59+
val p: Map<Primitive, String?>,
60+
val nn: Map<NonNullObject, String?>,
61+
val n: Map<NullableObject, String?>,
62+
val np: Map<NullablePrimitive, String?>,
63+
val tup: Map<TwoUnitPrimitive, String?>,
64+
)
65+
66+
@Test
67+
fun wrapped() {
68+
val src = """
69+
{
70+
"p":{"1":null},
71+
"nn":{"foo":null},
72+
"n":{"bar":null},
73+
"np":{"2":null},
74+
"tup":{"2":null}
75+
}
76+
""".trimIndent()
77+
val result = defaultMapper.readValue<Dst>(src)
78+
assertEquals(1, result.p.keys.first().v)
79+
assertEquals("foo", result.nn.keys.first().v)
80+
assertEquals("bar", result.n.keys.first().v)
81+
assertEquals(2, result.np.keys.first().v)
82+
assertEquals(2L, result.tup.keys.first().v)
83+
}
84+
}

0 commit comments

Comments
 (0)