Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ internal class IntValueClassUnboxConverter<T : Any>(
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Int>() {
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
}
Expand All @@ -175,7 +175,7 @@ internal class LongValueClassUnboxConverter<T : Any>(
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Long>() {
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
}
Expand All @@ -185,7 +185,7 @@ internal class StringValueClassUnboxConverter<T : Any>(
unboxMethod: Method,
) : ValueClassUnboxConverter<T, String?>() {
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?
}
Expand All @@ -195,7 +195,7 @@ internal class JavaUuidValueClassUnboxConverter<T : Any>(
unboxMethod: Method,
) : ValueClassUnboxConverter<T, UUID?>() {
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?
}
Expand All @@ -205,7 +205,7 @@ internal class GenericValueClassUnboxConverter<T : Any>(
override val unboxedType: Type,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Any?>() {
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ internal sealed class NoConversionCreatorBoxDeserializer<S, D : Any>(
) : WrapsNullableValueClassDeserializer<D>(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
Expand Down Expand Up @@ -190,7 +190,7 @@ internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer<S, D : Any>(
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)
Expand Down Expand Up @@ -222,7 +222,7 @@ internal class WrapsAnyValueClassBoxDeserializer<S, D : Any>(
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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ internal sealed class ValueClassKeyDeserializer<S, D : Any>(
// 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<D : Any>(
Expand Down Expand Up @@ -160,7 +160,7 @@ internal sealed class ValueClassKeyDeserializer<S, D : Any>(
creator: Method,
) : ValueClassKeyDeserializer<S, D>(
converter,
unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE),
unreflectAsTypeWithAccessibilityModification(creator, ANY_TO_ANY_METHOD_TYPE),
) {
override val unboxedClass: Class<*> = creator.returnType

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal sealed class ValueClassStaticJsonKeySerializer<T : Any>(
methodType: MethodType,
) : StdSerializer<T>(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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,39 +82,39 @@ internal sealed class ValueClassStaticJsonValueSerializer<T : Any>(
staticJsonValueGetter: Method,
) : ValueClassStaticJsonValueSerializer<T>(
converter,
unreflectAsType(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE),
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE),
)

internal class WrapsLong<T : Any>(
converter: LongValueClassUnboxConverter<T>,
staticJsonValueGetter: Method,
) : ValueClassStaticJsonValueSerializer<T>(
converter,
unreflectAsType(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE),
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE),
)

internal class WrapsString<T : Any>(
converter: StringValueClassUnboxConverter<T>,
staticJsonValueGetter: Method,
) : ValueClassStaticJsonValueSerializer<T>(
converter,
unreflectAsType(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE),
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE),
)

internal class WrapsJavaUuid<T : Any>(
converter: JavaUuidValueClassUnboxConverter<T>,
staticJsonValueGetter: Method,
) : ValueClassStaticJsonValueSerializer<T>(
converter,
unreflectAsType(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE),
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE),
)

internal class WrapsAny<T : Any>(
converter: GenericValueClassUnboxConverter<T>,
staticJsonValueGetter: Method,
) : ValueClassStaticJsonValueSerializer<T>(
converter,
unreflectAsType(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE),
unreflectAsTypeWithAccessibilityModification(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE),
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Primitive>("1")
assertEquals(1, result.v)
}

@Test
fun nonNullObjectTest() {
val result = defaultMapper.readValue<NonNullObject>(""""foo"""")
assertEquals("foo", result.v)
}

@Test
fun nullableObjectTest() {
val result = defaultMapper.readValue<NullableObject>(""""bar"""")
assertEquals("bar", result.v)
}

@Test
fun nullablePrimitiveTest() {
val result = defaultMapper.readValue<NullablePrimitive>("2")
assertEquals(2, result.v)
}

@Test
fun twoUnitPrimitiveTest() {
val result = defaultMapper.readValue<TwoUnitPrimitive>("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<Dto>(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)
}
}
Original file line number Diff line number Diff line change
@@ -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<Map<Primitive, String?>>("""{"1":null}""")
assertEquals(1, result.keys.first().v)
}

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

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

@Test
fun nullablePrimitive() {
val result = defaultMapper.readValue<Map<NullablePrimitive, String?>>("""{"2":null}""")
assertEquals(2, result.keys.first().v)
}

@Test
fun twoUnitPrimitive() {
val result = defaultMapper.readValue<Map<TwoUnitPrimitive, String?>>("""{"1":null}""")
assertEquals(1L, result.keys.first().v)
}
}

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

@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<Dst>(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)
}
}
Original file line number Diff line number Diff line change
@@ -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<PortCode> {
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<PortCode>("\"ABC\"")
assertEquals("ABC", result.value)
}
}