Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
01369cc
Update kogera version
k163377 May 25, 2025
fe15c05
Merge pull request #350 from ProjectMapK/k163377-patch-1
k163377 May 25, 2025
fc9fbc8
Bump org.junit:junit-bom from 5.12.2 to 5.13.0 in the dependencies group
dependabot[bot] May 30, 2025
cf9ea1f
Merge pull request #351 from ProjectMapK/dependabot/gradle/dependenci…
k163377 May 30, 2025
50c5f6b
Add comments
k163377 May 31, 2025
e59180f
Merge pull request #352 from ProjectMapK/java-duration-conversion
k163377 May 31, 2025
2bc3419
Add a test cases to serialize value classes in default
k163377 May 31, 2025
6e44f90
Fix redundant package names
k163377 May 31, 2025
3ea6dc7
Add a test cases to register serializers to Mapper and serialize valu…
k163377 May 31, 2025
70b0254
Merge pull request #353 from ProjectMapK/value-class-ser-tests
k163377 May 31, 2025
2fbbda1
Bump org.junit:junit-bom from 5.13.0 to 5.13.1 in the dependencies group
dependabot[bot] Jun 13, 2025
bf3011c
Merge pull request #354 from ProjectMapK/dependabot/gradle/dependenci…
k163377 Jun 13, 2025
93c718f
Modified to use MethodHandle for deserialization of value class
k163377 May 24, 2025
607ba85
Add specialized converter
k163377 May 24, 2025
7c07968
Add specialized deserializer
k163377 May 24, 2025
37ab0c3
Add converters for common classes as wrapped values
k163377 May 24, 2025
6907b39
Corrected the position of factory function definition
k163377 May 24, 2025
94245be
Fixed ValueClassUnboxConverter to use MethodHandle
k163377 May 24, 2025
c482b77
Add converters for common classes as wrapped values
k163377 May 24, 2025
e80e525
Cache the acquisition process to constants as much as possible
k163377 May 24, 2025
f1a1aac
Modified ValueClassStaticJsonValueSerializer to use MethodHandle
k163377 May 24, 2025
daa7171
Modified ValueClassKeyDeserializer to use MethodHandle
k163377 May 24, 2025
29f955c
Decrialization is made more efficient by reducing the asType conversions
k163377 May 24, 2025
1579484
Functionalized for simplicity
k163377 May 24, 2025
4ccec13
Fixed return value to a more concrete type
k163377 Jun 14, 2025
81a5c8f
Modified ValueClassStaticJsonKeySerializer to use MethodHandle
k163377 Jun 14, 2025
baadbd4
Merge pull request #355 from ProjectMapK/methodhandle
k163377 Jun 14, 2025
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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ val jacksonVersion = libs.versions.jackson.get()
val generatedSrcPath = "${layout.buildDirectory.get()}/generated/kotlin"

group = groupStr
version = "${jacksonVersion}-beta24"
version = "${jacksonVersion}-beta25"

repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ kotlin = "2.0.21" # Mainly for CI, it can be rewritten by environment variable.
jackson = "2.19.0"

# test libs
junit = "5.12.2"
junit = "5.13.1"

[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.WrapsNullableValueClassBoxDeserializer;
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.WrapsAnyValueClassBoxDeserializer;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KClass;
import org.jetbrains.annotations.NotNull;
Expand All @@ -15,7 +15,7 @@

/**
* An interface to be inherited by JsonDeserializer that handles value classes that may wrap nullable.
* @see WrapsNullableValueClassBoxDeserializer for implementation.
* @see WrapsAnyValueClassBoxDeserializer for implementation.
*/
// To ensure maximum compatibility with StdDeserializer, this class is written in Java.
public abstract class WrapsNullableValueClassDeserializer<D> extends StdDeserializer<D> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,171 @@ package io.github.projectmapk.jackson.module.kogera
import com.fasterxml.jackson.databind.JavaType
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 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

internal sealed class ValueClassBoxConverter<S : Any?, D : Any> : StdConverter<S, D>() {
abstract val boxedClass: Class<D>
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 -> IntValueClassBoxConverter(valueClass)
LONG_CLASS -> LongValueClassBoxConverter(valueClass)
STRING_CLASS -> StringValueClassBoxConverter(valueClass)
JAVA_UUID_CLASS -> 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<S : Any?, D : Any> : ValueClassBoxConverter<S, D>()
}

// region: Converters for common classes as wrapped values, add as needed.
internal class IntValueClassBoxConverter<D : Any>(
override val boxedClass: Class<D>,
) : ValueClassBoxConverter.Specified<Int, D>() {
override val boxHandle: MethodHandle = rawBoxHandle(INT_CLASS).asType(INT_TO_ANY_METHOD_TYPE)

@Suppress("UNCHECKED_CAST")
override fun convert(value: Int): D = boxHandle.invokeExact(value) as D
}

internal class LongValueClassBoxConverter<D : Any>(
override val boxedClass: Class<D>,
) : ValueClassBoxConverter.Specified<Long, D>() {
override val boxHandle: MethodHandle = rawBoxHandle(LONG_CLASS).asType(LONG_TO_ANY_METHOD_TYPE)

@Suppress("UNCHECKED_CAST")
override fun convert(value: Long): D = boxHandle.invokeExact(value) as D
}

internal class StringValueClassBoxConverter<D : Any>(
override val boxedClass: Class<D>,
) : ValueClassBoxConverter.Specified<String?, D>() {
override val boxHandle: MethodHandle = rawBoxHandle(STRING_CLASS).asType(STRING_TO_ANY_METHOD_TYPE)

@Suppress("UNCHECKED_CAST")
override fun convert(value: String?): D = boxHandle.invokeExact(value) as D
}

internal class JavaUuidValueClassBoxConverter<D : Any>(
override val boxedClass: Class<D>,
) : ValueClassBoxConverter.Specified<UUID?, D>() {
override val boxHandle: MethodHandle = rawBoxHandle(JAVA_UUID_CLASS).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 ValueClassBoxConverter<S : Any?, D : Any>(
internal class GenericValueClassBoxConverter<S : Any?, D : Any>(
unboxedClass: Class<S>,
val boxedClass: Class<D>,
) : StdConverter<S, D>() {
private val boxMethod = boxedClass.getDeclaredMethod("box-impl", unboxedClass).apply {
ClassUtil.checkAndFixAccess(this, false)
}
override val boxedClass: Class<D>,
) : ValueClassBoxConverter<S, D>() {
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<S : Any, D : Any?> : StdConverter<S, D>() {
abstract val valueClass: Class<S>
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) }
}

internal class ValueClassUnboxConverter<T : Any>(val valueClass: Class<T>) : StdConverter<T, Any?>() {
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
ClassUtil.checkAndFixAccess(this, false)
companion object {
fun create(valueClass: Class<*>): ValueClassUnboxConverter<*, *> {
val unboxMethod = valueClass.getDeclaredMethod("unbox-impl")
val unboxedType = unboxMethod.genericReturnType

return when (unboxedType) {
INT_CLASS -> IntValueClassUnboxConverter(valueClass, unboxMethod)
LONG_CLASS -> LongValueClassUnboxConverter(valueClass, unboxMethod)
STRING_CLASS -> StringValueClassUnboxConverter(valueClass, unboxMethod)
JAVA_UUID_CLASS -> JavaUuidValueClassUnboxConverter(valueClass, unboxMethod)
else -> GenericValueClassUnboxConverter(valueClass, unboxedType, unboxMethod)
}
}
}
}

override fun convert(value: T): Any? = unboxMethod.invoke(value)
internal class IntValueClassUnboxConverter<T : Any>(
override val valueClass: Class<T>,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Int>() {
override val unboxedType: Type get() = INT_CLASS
override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_INT_METHOD_TYPE)

override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass)
override fun getOutputType(
typeFactory: TypeFactory,
): JavaType = typeFactory.constructType(unboxMethod.genericReturnType)
override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int
}

val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
internal class LongValueClassUnboxConverter<T : Any>(
override val valueClass: Class<T>,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Long>() {
override val unboxedType: Type get() = LONG_CLASS
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<T : Any>(
override val valueClass: Class<T>,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, String?>() {
override val unboxedType: Type get() = STRING_CLASS
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<T : Any>(
override val valueClass: Class<T>,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, UUID?>() {
override val unboxedType: Type get() = JAVA_UUID_CLASS
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<T : Any>(
override val valueClass: Class<T>,
override val unboxedType: Type,
unboxMethod: Method,
) : ValueClassUnboxConverter<T, Any?>() {
override val unboxHandle: MethodHandle = unreflectAsType(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 @@ -3,6 +3,9 @@ package io.github.projectmapk.jackson.module.kogera
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import io.github.projectmapk.jackson.module.kogera.annotation.JsonKUnbox
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.lang.reflect.AnnotatedElement
import java.lang.reflect.Constructor
import java.lang.reflect.Method
Expand All @@ -21,49 +24,6 @@ internal fun Class<*>.toKmClass(): KmClass? = getAnnotation(METADATA_CLASS)?.let

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

private val primitiveClassToDesc = mapOf(
Byte::class.java to 'B',
Char::class.java to 'C',
Double::class.java to 'D',
Float::class.java to 'F',
Int::class.java to 'I',
Long::class.java to 'J',
Short::class.java to 'S',
Boolean::class.java to 'Z',
Void.TYPE to 'V',
)

// -> this.name.replace(".", "/")
private fun Class<*>.descName(): String {
val replaced = name.toCharArray().apply {
for (i in indices) {
if (this[i] == '.') this[i] = '/'
}
}
return String(replaced)
}

private fun StringBuilder.appendDescriptor(clazz: Class<*>): StringBuilder = when {
clazz.isPrimitive -> append(primitiveClassToDesc.getValue(clazz))
clazz.isArray -> append('[').appendDescriptor(clazz.componentType)
else -> append("L${clazz.descName()};")
}

// -> this.joinToString(separator = "", prefix = "(", postfix = ")") { it.descriptor }
internal fun Array<Class<*>>.toDescBuilder(): StringBuilder = this
.fold(StringBuilder("(")) { acc, cur -> acc.appendDescriptor(cur) }
.append(')')

internal fun Constructor<*>.toSignature(): JvmMethodSignature = JvmMethodSignature(
"<init>",
parameterTypes.toDescBuilder().append('V').toString(),
)

internal fun Method.toSignature(): JvmMethodSignature = JvmMethodSignature(
this.name,
parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString(),
)

internal val defaultConstructorMarker: Class<*> by lazy {
Class.forName("kotlin.jvm.internal.DefaultConstructorMarker")
}
Expand Down Expand Up @@ -105,4 +65,64 @@ internal val JSON_PROPERTY_CLASS = JsonProperty::class.java
internal val JSON_K_UNBOX_CLASS = JsonKUnbox::class.java
internal val KOTLIN_DURATION_CLASS = KotlinDuration::class.java
internal val CLOSED_FLOATING_POINT_RANGE_CLASS = ClosedFloatingPointRange::class.java
internal val INT_CLASS = Int::class.java
internal val LONG_CLASS = Long::class.java
internal val STRING_CLASS = String::class.java
internal val JAVA_UUID_CLASS = java.util.UUID::class.java
internal val ANY_CLASS = Any::class.java

internal val ANY_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, ANY_CLASS)
internal val ANY_TO_INT_METHOD_TYPE = MethodType.methodType(INT_CLASS, ANY_CLASS)
internal val ANY_TO_LONG_METHOD_TYPE = MethodType.methodType(LONG_CLASS, ANY_CLASS)
internal val ANY_TO_STRING_METHOD_TYPE = MethodType.methodType(STRING_CLASS, ANY_CLASS)
internal val ANY_TO_JAVA_UUID_METHOD_TYPE = MethodType.methodType(JAVA_UUID_CLASS, ANY_CLASS)
internal val INT_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, INT_CLASS)
internal val LONG_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, LONG_CLASS)
internal val STRING_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, STRING_CLASS)
internal val JAVA_UUID_TO_ANY_METHOD_TYPE = MethodType.methodType(ANY_CLASS, JAVA_UUID_CLASS)

internal fun unreflect(method: Method): MethodHandle = MethodHandles.lookup().unreflect(method)
internal fun unreflectAsType(method: Method, type: MethodType): MethodHandle = unreflect(method).asType(type)

private val primitiveClassToDesc = mapOf(
Byte::class.java to 'B',
Char::class.java to 'C',
Double::class.java to 'D',
Float::class.java to 'F',
INT_CLASS to 'I',
LONG_CLASS to 'J',
Short::class.java to 'S',
Boolean::class.java to 'Z',
Void.TYPE to 'V',
)

// -> this.name.replace(".", "/")
private fun Class<*>.descName(): String {
val replaced = name.toCharArray().apply {
for (i in indices) {
if (this[i] == '.') this[i] = '/'
}
}
return String(replaced)
}

private fun StringBuilder.appendDescriptor(clazz: Class<*>): StringBuilder = when {
clazz.isPrimitive -> append(primitiveClassToDesc.getValue(clazz))
clazz.isArray -> append('[').appendDescriptor(clazz.componentType)
else -> append("L${clazz.descName()};")
}

// -> this.joinToString(separator = "", prefix = "(", postfix = ")") { it.descriptor }
internal fun Array<Class<*>>.toDescBuilder(): StringBuilder = this
.fold(StringBuilder("(")) { acc, cur -> acc.appendDescriptor(cur) }
.append(')')

internal fun Constructor<*>.toSignature(): JvmMethodSignature = JvmMethodSignature(
"<init>",
parameterTypes.toDescBuilder().append('V').toString(),
)

internal fun Method.toSignature(): JvmMethodSignature = JvmMethodSignature(
this.name,
parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria
) : OtherCacheKey<Class<*>, io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter<*, *>>()
class ValueClassUnboxConverter(
override val key: Class<*>,
) : OtherCacheKey<Class<*>, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*>>()
) : OtherCacheKey<Class<*>, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*, *>>()
}

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

fun getValueClassBoxConverter(unboxedClass: Class<*>, valueClass: Class<*>): ValueClassBoxConverter<*, *> {
val key = OtherCacheKey.ValueClassBoxConverter(valueClass)
return find(key) ?: putIfAbsent(key, ValueClassBoxConverter(unboxedClass, valueClass))
return find(key) ?: putIfAbsent(key, ValueClassBoxConverter.create(unboxedClass, valueClass))
}

fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*> {
fun getValueClassUnboxConverter(valueClass: Class<*>): ValueClassUnboxConverter<*, *> {
val key = OtherCacheKey.ValueClassUnboxConverter(valueClass)
return find(key) ?: putIfAbsent(key, ValueClassUnboxConverter(valueClass))
return find(key) ?: putIfAbsent(key, ValueClassUnboxConverter.create(valueClass))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ internal class KotlinFallbackAnnotationIntrospector(
override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) {
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
is AnnotatedMethod -> cache.findBoxedReturnType(a.member)?.let {
// To make annotations that process JavaDuration work,
// it is necessary to set up the conversion to JavaDuration here.
// This conversion will cause the deserialization settings for KotlinDuration to be ignored.
if (useJavaDurationConversion && it == KOTLIN_DURATION_CLASS) {
// For early return, the same process is placed as the branch regarding AnnotatedClass.
if (a.rawReturnType == KOTLIN_DURATION_CLASS) {
KotlinToJavaDurationConverter
} else {
Expand Down
Loading
Loading