Skip to content

Commit e21dab9

Browse files
authored
Merge pull request #356 from ProjectMapK/develop
Release 2025-06-14 12:37:53 +0000
2 parents c6bdd17 + baadbd4 commit e21dab9

File tree

24 files changed

+983
-134
lines changed

24 files changed

+983
-134
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ val jacksonVersion = libs.versions.jackson.get()
1818
val generatedSrcPath = "${layout.buildDirectory.get()}/generated/kotlin"
1919

2020
group = groupStr
21-
version = "${jacksonVersion}-beta24"
21+
version = "${jacksonVersion}-beta25"
2222

2323
repositories {
2424
mavenCentral()

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ kotlin = "2.0.21" # Mainly for CI, it can be rewritten by environment variable.
33
jackson = "2.19.0"
44

55
# test libs
6-
junit = "5.12.2"
6+
junit = "5.13.1"
77

88
[libraries]
99
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" }

src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.fasterxml.jackson.databind.DeserializationContext;
66
import com.fasterxml.jackson.databind.JavaType;
77
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
8-
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.WrapsNullableValueClassBoxDeserializer;
8+
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.WrapsAnyValueClassBoxDeserializer;
99
import kotlin.jvm.JvmClassMappingKt;
1010
import kotlin.reflect.KClass;
1111
import org.jetbrains.annotations.NotNull;
@@ -15,7 +15,7 @@
1515

1616
/**
1717
* An interface to be inherited by JsonDeserializer that handles value classes that may wrap nullable.
18-
* @see WrapsNullableValueClassBoxDeserializer for implementation.
18+
* @see WrapsAnyValueClassBoxDeserializer for implementation.
1919
*/
2020
// To ensure maximum compatibility with StdDeserializer, this class is written in Java.
2121
public abstract class WrapsNullableValueClassDeserializer<D> extends StdDeserializer<D> {

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt

Lines changed: 149 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,171 @@ package io.github.projectmapk.jackson.module.kogera
33
import com.fasterxml.jackson.databind.JavaType
44
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
55
import com.fasterxml.jackson.databind.type.TypeFactory
6-
import com.fasterxml.jackson.databind.util.ClassUtil
76
import com.fasterxml.jackson.databind.util.StdConverter
7+
import java.lang.invoke.MethodHandle
8+
import java.lang.invoke.MethodHandles
9+
import java.lang.invoke.MethodType
10+
import java.lang.reflect.Method
11+
import java.lang.reflect.Type
12+
import java.util.UUID
13+
14+
internal sealed class ValueClassBoxConverter<S : Any?, D : Any> : StdConverter<S, D>() {
15+
abstract val boxedClass: Class<D>
16+
abstract val boxHandle: MethodHandle
17+
18+
protected fun rawBoxHandle(
19+
unboxedClass: Class<*>,
20+
): MethodHandle = MethodHandles.lookup().findStatic(
21+
boxedClass,
22+
"box-impl",
23+
MethodType.methodType(boxedClass, unboxedClass),
24+
)
25+
26+
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
27+
28+
companion object {
29+
fun create(
30+
unboxedClass: Class<*>,
31+
valueClass: Class<*>,
32+
): ValueClassBoxConverter<*, *> = when (unboxedClass) {
33+
INT_CLASS -> IntValueClassBoxConverter(valueClass)
34+
LONG_CLASS -> LongValueClassBoxConverter(valueClass)
35+
STRING_CLASS -> StringValueClassBoxConverter(valueClass)
36+
JAVA_UUID_CLASS -> JavaUuidValueClassBoxConverter(valueClass)
37+
else -> GenericValueClassBoxConverter(unboxedClass, valueClass)
38+
}
39+
}
40+
41+
// If the wrapped type is explicitly specified, it is inherited for the sake of distinction
42+
internal sealed class Specified<S : Any?, D : Any> : ValueClassBoxConverter<S, D>()
43+
}
44+
45+
// region: Converters for common classes as wrapped values, add as needed.
46+
internal class IntValueClassBoxConverter<D : Any>(
47+
override val boxedClass: Class<D>,
48+
) : ValueClassBoxConverter.Specified<Int, D>() {
49+
override val boxHandle: MethodHandle = rawBoxHandle(INT_CLASS).asType(INT_TO_ANY_METHOD_TYPE)
50+
51+
@Suppress("UNCHECKED_CAST")
52+
override fun convert(value: Int): D = boxHandle.invokeExact(value) as D
53+
}
54+
55+
internal class LongValueClassBoxConverter<D : Any>(
56+
override val boxedClass: Class<D>,
57+
) : ValueClassBoxConverter.Specified<Long, D>() {
58+
override val boxHandle: MethodHandle = rawBoxHandle(LONG_CLASS).asType(LONG_TO_ANY_METHOD_TYPE)
59+
60+
@Suppress("UNCHECKED_CAST")
61+
override fun convert(value: Long): D = boxHandle.invokeExact(value) as D
62+
}
63+
64+
internal class StringValueClassBoxConverter<D : Any>(
65+
override val boxedClass: Class<D>,
66+
) : ValueClassBoxConverter.Specified<String?, D>() {
67+
override val boxHandle: MethodHandle = rawBoxHandle(STRING_CLASS).asType(STRING_TO_ANY_METHOD_TYPE)
68+
69+
@Suppress("UNCHECKED_CAST")
70+
override fun convert(value: String?): D = boxHandle.invokeExact(value) as D
71+
}
72+
73+
internal class JavaUuidValueClassBoxConverter<D : Any>(
74+
override val boxedClass: Class<D>,
75+
) : ValueClassBoxConverter.Specified<UUID?, D>() {
76+
override val boxHandle: MethodHandle = rawBoxHandle(JAVA_UUID_CLASS).asType(JAVA_UUID_TO_ANY_METHOD_TYPE)
77+
78+
@Suppress("UNCHECKED_CAST")
79+
override fun convert(value: UUID?): D = boxHandle.invokeExact(value) as D
80+
}
81+
// endregion
882

983
/**
1084
* A converter that only performs box processing for the value class.
1185
* Note that constructor-impl is not called.
1286
* @param S is nullable because value corresponds to a nullable value class.
1387
* see [io.github.projectmapk.jackson.module.kogera.annotationIntrospector.KotlinFallbackAnnotationIntrospector.findNullSerializer]
1488
*/
15-
internal class ValueClassBoxConverter<S : Any?, D : Any>(
89+
internal class GenericValueClassBoxConverter<S : Any?, D : Any>(
1690
unboxedClass: Class<S>,
17-
val boxedClass: Class<D>,
18-
) : StdConverter<S, D>() {
19-
private val boxMethod = boxedClass.getDeclaredMethod("box-impl", unboxedClass).apply {
20-
ClassUtil.checkAndFixAccess(this, false)
21-
}
91+
override val boxedClass: Class<D>,
92+
) : ValueClassBoxConverter<S, D>() {
93+
override val boxHandle: MethodHandle = rawBoxHandle(unboxedClass).asType(ANY_TO_ANY_METHOD_TYPE)
2294

2395
@Suppress("UNCHECKED_CAST")
24-
override fun convert(value: S): D = boxMethod.invoke(null, value) as D
96+
override fun convert(value: S): D = boxHandle.invokeExact(value) as D
97+
}
98+
99+
internal sealed class ValueClassUnboxConverter<S : Any, D : Any?> : StdConverter<S, D>() {
100+
abstract val valueClass: Class<S>
101+
abstract val unboxedType: Type
102+
abstract val unboxHandle: MethodHandle
103+
104+
final override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass)
105+
final override fun getOutputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(unboxedType)
25106

26107
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
27-
}
28108

29-
internal class ValueClassUnboxConverter<T : Any>(val valueClass: Class<T>) : StdConverter<T, Any?>() {
30-
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
31-
ClassUtil.checkAndFixAccess(this, false)
109+
companion object {
110+
fun create(valueClass: Class<*>): ValueClassUnboxConverter<*, *> {
111+
val unboxMethod = valueClass.getDeclaredMethod("unbox-impl")
112+
val unboxedType = unboxMethod.genericReturnType
113+
114+
return when (unboxedType) {
115+
INT_CLASS -> IntValueClassUnboxConverter(valueClass, unboxMethod)
116+
LONG_CLASS -> LongValueClassUnboxConverter(valueClass, unboxMethod)
117+
STRING_CLASS -> StringValueClassUnboxConverter(valueClass, unboxMethod)
118+
JAVA_UUID_CLASS -> JavaUuidValueClassUnboxConverter(valueClass, unboxMethod)
119+
else -> GenericValueClassUnboxConverter(valueClass, unboxedType, unboxMethod)
120+
}
121+
}
32122
}
123+
}
33124

34-
override fun convert(value: T): Any? = unboxMethod.invoke(value)
125+
internal class IntValueClassUnboxConverter<T : Any>(
126+
override val valueClass: Class<T>,
127+
unboxMethod: Method,
128+
) : ValueClassUnboxConverter<T, Int>() {
129+
override val unboxedType: Type get() = INT_CLASS
130+
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_INT_METHOD_TYPE)
35131

36-
override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass)
37-
override fun getOutputType(
38-
typeFactory: TypeFactory,
39-
): JavaType = typeFactory.constructType(unboxMethod.genericReturnType)
132+
override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int
133+
}
40134

41-
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
135+
internal class LongValueClassUnboxConverter<T : Any>(
136+
override val valueClass: Class<T>,
137+
unboxMethod: Method,
138+
) : ValueClassUnboxConverter<T, Long>() {
139+
override val unboxedType: Type get() = LONG_CLASS
140+
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_LONG_METHOD_TYPE)
141+
142+
override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long
143+
}
144+
145+
internal class StringValueClassUnboxConverter<T : Any>(
146+
override val valueClass: Class<T>,
147+
unboxMethod: Method,
148+
) : ValueClassUnboxConverter<T, String?>() {
149+
override val unboxedType: Type get() = STRING_CLASS
150+
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_STRING_METHOD_TYPE)
151+
152+
override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String?
153+
}
154+
155+
internal class JavaUuidValueClassUnboxConverter<T : Any>(
156+
override val valueClass: Class<T>,
157+
unboxMethod: Method,
158+
) : ValueClassUnboxConverter<T, UUID?>() {
159+
override val unboxedType: Type get() = JAVA_UUID_CLASS
160+
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE)
161+
162+
override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID?
163+
}
164+
165+
internal class GenericValueClassUnboxConverter<T : Any>(
166+
override val valueClass: Class<T>,
167+
override val unboxedType: Type,
168+
unboxMethod: Method,
169+
) : ValueClassUnboxConverter<T, Any?>() {
170+
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE)
171+
172+
override fun convert(value: T): Any? = unboxHandle.invokeExact(value)
42173
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package io.github.projectmapk.jackson.module.kogera
33
import com.fasterxml.jackson.annotation.JsonCreator
44
import com.fasterxml.jackson.annotation.JsonProperty
55
import io.github.projectmapk.jackson.module.kogera.annotation.JsonKUnbox
6+
import java.lang.invoke.MethodHandle
7+
import java.lang.invoke.MethodHandles
8+
import java.lang.invoke.MethodType
69
import java.lang.reflect.AnnotatedElement
710
import java.lang.reflect.Constructor
811
import java.lang.reflect.Method
@@ -21,49 +24,6 @@ internal fun Class<*>.toKmClass(): KmClass? = getAnnotation(METADATA_CLASS)?.let
2124

2225
internal fun Class<*>.isUnboxableValueClass() = this.isAnnotationPresent(JVM_INLINE_CLASS)
2326

24-
private val primitiveClassToDesc = mapOf(
25-
Byte::class.java to 'B',
26-
Char::class.java to 'C',
27-
Double::class.java to 'D',
28-
Float::class.java to 'F',
29-
Int::class.java to 'I',
30-
Long::class.java to 'J',
31-
Short::class.java to 'S',
32-
Boolean::class.java to 'Z',
33-
Void.TYPE to 'V',
34-
)
35-
36-
// -> this.name.replace(".", "/")
37-
private fun Class<*>.descName(): String {
38-
val replaced = name.toCharArray().apply {
39-
for (i in indices) {
40-
if (this[i] == '.') this[i] = '/'
41-
}
42-
}
43-
return String(replaced)
44-
}
45-
46-
private fun StringBuilder.appendDescriptor(clazz: Class<*>): StringBuilder = when {
47-
clazz.isPrimitive -> append(primitiveClassToDesc.getValue(clazz))
48-
clazz.isArray -> append('[').appendDescriptor(clazz.componentType)
49-
else -> append("L${clazz.descName()};")
50-
}
51-
52-
// -> this.joinToString(separator = "", prefix = "(", postfix = ")") { it.descriptor }
53-
internal fun Array<Class<*>>.toDescBuilder(): StringBuilder = this
54-
.fold(StringBuilder("(")) { acc, cur -> acc.appendDescriptor(cur) }
55-
.append(')')
56-
57-
internal fun Constructor<*>.toSignature(): JvmMethodSignature = JvmMethodSignature(
58-
"<init>",
59-
parameterTypes.toDescBuilder().append('V').toString(),
60-
)
61-
62-
internal fun Method.toSignature(): JvmMethodSignature = JvmMethodSignature(
63-
this.name,
64-
parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString(),
65-
)
66-
6727
internal val defaultConstructorMarker: Class<*> by lazy {
6828
Class.forName("kotlin.jvm.internal.DefaultConstructorMarker")
6929
}
@@ -105,4 +65,64 @@ internal val JSON_PROPERTY_CLASS = JsonProperty::class.java
10565
internal val JSON_K_UNBOX_CLASS = JsonKUnbox::class.java
10666
internal val KOTLIN_DURATION_CLASS = KotlinDuration::class.java
10767
internal val CLOSED_FLOATING_POINT_RANGE_CLASS = ClosedFloatingPointRange::class.java
68+
internal val INT_CLASS = Int::class.java
69+
internal val LONG_CLASS = Long::class.java
70+
internal val STRING_CLASS = String::class.java
71+
internal val JAVA_UUID_CLASS = java.util.UUID::class.java
10872
internal val ANY_CLASS = Any::class.java
73+
74+
internal val ANY_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, ANY_CLASS)
75+
internal val ANY_TO_INT_METHOD_TYPE = MethodType.methodType(INT_CLASS, ANY_CLASS)
76+
internal val ANY_TO_LONG_METHOD_TYPE = MethodType.methodType(LONG_CLASS, ANY_CLASS)
77+
internal val ANY_TO_STRING_METHOD_TYPE = MethodType.methodType(STRING_CLASS, ANY_CLASS)
78+
internal val ANY_TO_JAVA_UUID_METHOD_TYPE = MethodType.methodType(JAVA_UUID_CLASS, ANY_CLASS)
79+
internal val INT_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, INT_CLASS)
80+
internal val LONG_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, LONG_CLASS)
81+
internal val STRING_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, STRING_CLASS)
82+
internal val JAVA_UUID_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, JAVA_UUID_CLASS)
83+
84+
internal fun unreflect(method: Method): MethodHandle = MethodHandles.lookup().unreflect(method)
85+
internal fun unreflectAsType(method: Method, type: MethodType): MethodHandle = unreflect(method).asType(type)
86+
87+
private val primitiveClassToDesc = mapOf(
88+
Byte::class.java to 'B',
89+
Char::class.java to 'C',
90+
Double::class.java to 'D',
91+
Float::class.java to 'F',
92+
INT_CLASS to 'I',
93+
LONG_CLASS to 'J',
94+
Short::class.java to 'S',
95+
Boolean::class.java to 'Z',
96+
Void.TYPE to 'V',
97+
)
98+
99+
// -> this.name.replace(".", "/")
100+
private fun Class<*>.descName(): String {
101+
val replaced = name.toCharArray().apply {
102+
for (i in indices) {
103+
if (this[i] == '.') this[i] = '/'
104+
}
105+
}
106+
return String(replaced)
107+
}
108+
109+
private fun StringBuilder.appendDescriptor(clazz: Class<*>): StringBuilder = when {
110+
clazz.isPrimitive -> append(primitiveClassToDesc.getValue(clazz))
111+
clazz.isArray -> append('[').appendDescriptor(clazz.componentType)
112+
else -> append("L${clazz.descName()};")
113+
}
114+
115+
// -> this.joinToString(separator = "", prefix = "(", postfix = ")") { it.descriptor }
116+
internal fun Array<Class<*>>.toDescBuilder(): StringBuilder = this
117+
.fold(StringBuilder("(")) { acc, cur -> acc.appendDescriptor(cur) }
118+
.append(')')
119+
120+
internal fun Constructor<*>.toSignature(): JvmMethodSignature = JvmMethodSignature(
121+
"<init>",
122+
parameterTypes.toDescBuilder().append('V').toString(),
123+
)
124+
125+
internal fun Method.toSignature(): JvmMethodSignature = JvmMethodSignature(
126+
this.name,
127+
parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString(),
128+
)

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria
3434
) : OtherCacheKey<Class<*>, io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter<*, *>>()
3535
class ValueClassUnboxConverter(
3636
override val key: Class<*>,
37-
) : OtherCacheKey<Class<*>, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*>>()
37+
) : OtherCacheKey<Class<*>, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*, *>>()
3838
}
3939

4040
private val cache = LRUMap<Any, Any>(initialCacheSize, maxCacheSize)
@@ -93,11 +93,11 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria
9393

9494
fun getValueClassBoxConverter(unboxedClass: Class<*>, valueClass: Class<*>): ValueClassBoxConverter<*, *> {
9595
val key = OtherCacheKey.ValueClassBoxConverter(valueClass)
96-
return find(key) ?: putIfAbsent(key, ValueClassBoxConverter(unboxedClass, valueClass))
96+
return find(key) ?: putIfAbsent(key, ValueClassBoxConverter.create(unboxedClass, valueClass))
9797
}
9898

99-
fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> {
99+
fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*, *> {
100100
val key = OtherCacheKey.ValueClassUnboxConverter(valueClass)
101-
return find(key) ?: putIfAbsent(key, ValueClassUnboxConverter(valueClass))
101+
return find(key) ?: putIfAbsent(key, ValueClassUnboxConverter.create(valueClass))
102102
}
103103
}

src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ internal class KotlinFallbackAnnotationIntrospector(
8080
override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) {
8181
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
8282
is AnnotatedMethod -> cache.findBoxedReturnType(a.member)?.let {
83+
// To make annotations that process JavaDuration work,
84+
// it is necessary to set up the conversion to JavaDuration here.
85+
// This conversion will cause the deserialization settings for KotlinDuration to be ignored.
8386
if (useJavaDurationConversion && it == KOTLIN_DURATION_CLASS) {
87+
// For early return, the same process is placed as the branch regarding AnnotatedClass.
8488
if (a.rawReturnType == KOTLIN_DURATION_CLASS) {
8589
KotlinToJavaDurationConverter
8690
} else {

0 commit comments

Comments
 (0)