diff --git a/build.gradle.kts b/build.gradle.kts index 8c883c3d..d8d577a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af4d776b..614c698c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java b/src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java index 852dd097..12e19136 100644 --- a/src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java +++ b/src/main/java/io/github/projectmapk/jackson/module/kogera/deser/WrapsNullableValueClassDeserializer.java @@ -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; @@ -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 extends StdDeserializer { diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt index 5d9baf23..7cfa252a 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/Converters.kt @@ -3,8 +3,82 @@ 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 : StdConverter() { + abstract val boxedClass: Class + 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 : ValueClassBoxConverter() +} + +// region: Converters for common classes as wrapped values, add as needed. +internal class IntValueClassBoxConverter( + override val boxedClass: Class, +) : ValueClassBoxConverter.Specified() { + 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( + override val boxedClass: Class, +) : ValueClassBoxConverter.Specified() { + 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( + override val boxedClass: Class, +) : ValueClassBoxConverter.Specified() { + 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( + override val boxedClass: Class, +) : ValueClassBoxConverter.Specified() { + 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. @@ -12,31 +86,88 @@ import com.fasterxml.jackson.databind.util.StdConverter * @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( +internal class GenericValueClassBoxConverter( unboxedClass: Class, - val boxedClass: Class, -) : StdConverter() { - private val boxMethod = boxedClass.getDeclaredMethod("box-impl", unboxedClass).apply { - ClassUtil.checkAndFixAccess(this, false) - } + override val boxedClass: Class, +) : ValueClassBoxConverter() { + 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 : StdConverter() { + abstract val valueClass: Class + 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(val valueClass: Class) : StdConverter() { - 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( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + 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( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + 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( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + 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( + override val valueClass: Class, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + 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( + override val valueClass: Class, + override val unboxedType: Type, + unboxMethod: Method, +) : ValueClassUnboxConverter() { + override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE) + + override fun convert(value: T): Any? = unboxHandle.invokeExact(value) } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt index d6f51e64..d77871e7 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/InternalCommons.kt @@ -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 @@ -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>.toDescBuilder(): StringBuilder = this - .fold(StringBuilder("(")) { acc, cur -> acc.appendDescriptor(cur) } - .append(')') - -internal fun Constructor<*>.toSignature(): JvmMethodSignature = JvmMethodSignature( - "", - 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") } @@ -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>.toDescBuilder(): StringBuilder = this + .fold(StringBuilder("(")) { acc, cur -> acc.appendDescriptor(cur) } + .append(')') + +internal fun Constructor<*>.toSignature(): JvmMethodSignature = JvmMethodSignature( + "", + parameterTypes.toDescBuilder().append('V').toString(), +) + +internal fun Method.toSignature(): JvmMethodSignature = JvmMethodSignature( + this.name, + parameterTypes.toDescBuilder().appendDescriptor(this.returnType).toString(), +) diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt index 01b8a460..a600b080 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ReflectionCache.kt @@ -34,7 +34,7 @@ internal class ReflectionCache(initialCacheSize: Int, maxCacheSize: Int) : Seria ) : OtherCacheKey, io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter<*, *>>() class ValueClassUnboxConverter( override val key: Class<*>, - ) : OtherCacheKey, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*>>() + ) : OtherCacheKey, io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter<*, *>>() } private val cache = LRUMap(initialCacheSize, maxCacheSize) @@ -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)) } } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt index ed302303..776780ce 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/annotationIntrospector/KotlinFallbackAnnotationIntrospector.kt @@ -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 { diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt index f0531313..387d906c 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinDeserializers.kt @@ -9,9 +9,19 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.module.SimpleDeserializers -import com.fasterxml.jackson.databind.util.ClassUtil +import io.github.projectmapk.jackson.module.kogera.ANY_CLASS +import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.INT_CLASS +import io.github.projectmapk.jackson.module.kogera.IntValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.JAVA_UUID_CLASS +import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.KotlinDuration +import io.github.projectmapk.jackson.module.kogera.LONG_CLASS +import io.github.projectmapk.jackson.module.kogera.LongValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.STRING_CLASS +import io.github.projectmapk.jackson.module.kogera.StringValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationConverter import io.github.projectmapk.jackson.module.kogera.deser.WrapsNullableValueClassDeserializer @@ -19,8 +29,13 @@ import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass import io.github.projectmapk.jackson.module.kogera.toSignature +import io.github.projectmapk.jackson.module.kogera.unreflect +import io.github.projectmapk.jackson.module.kogera.unreflectAsType +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.lang.reflect.Modifier +import java.util.UUID internal object SequenceDeserializer : StdDeserializer>(Sequence::class.java) { private fun readResolve(): Any = SequenceDeserializer @@ -88,14 +103,98 @@ internal object ULongDeserializer : StdDeserializer(ULong::class.java) { .readWithRangeCheck(p, p.bigIntegerValue) } -internal class WrapsNullableValueClassBoxDeserializer( - private val creator: Method, - private val converter: ValueClassBoxConverter, +// If the creator does not perform type conversion, implement a unique deserializer for each for fast invocation. +internal sealed class NoConversionCreatorBoxDeserializer( + creator: Method, + converter: ValueClassBoxConverter, ) : WrapsNullableValueClassDeserializer(converter.boxedClass) { - private val inputType: Class<*> = creator.parameterTypes[0] + protected abstract val inputType: Class<*> + protected val handle: MethodHandle = MethodHandles + .filterReturnValue(unreflect(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 + + // Cache the result of wrapping null, since the result is always expected to be the same. + @get:JvmName("boxedNullValue") + private val boxedNullValue: D by lazy { + // For the sake of commonality, it is unavoidably called without checking. + // It is controlled by KotlinValueInstantiator, so it is not expected to reach this branch. + @Suppress("UNCHECKED_CAST") + invokeExact(null as S) + } + + final override fun getBoxedNullValue(): D = boxedNullValue + + final override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + @Suppress("UNCHECKED_CAST") + return invokeExact(p.readValueAs(inputType) as S) + } + + internal class WrapsInt( + creator: Method, + converter: IntValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = INT_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Int): D = handle.invokeExact(value) as D + } + + internal class WrapsLong( + creator: Method, + converter: LongValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = LONG_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Long): D = handle.invokeExact(value) as D + } + + internal class WrapsString( + creator: Method, + converter: StringValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = STRING_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: String?): D = handle.invokeExact(value) as D + } + + internal class WrapsJavaUuid( + creator: Method, + converter: JavaUuidValueClassBoxConverter, + ) : NoConversionCreatorBoxDeserializer(creator, converter) { + override val inputType get() = JAVA_UUID_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D + } + + companion object { + fun create(creator: Method, converter: ValueClassBoxConverter.Specified) = when (converter) { + is IntValueClassBoxConverter -> WrapsInt(creator, converter) + is LongValueClassBoxConverter -> WrapsLong(creator, converter) + is StringValueClassBoxConverter -> WrapsString(creator, converter) + is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(creator, converter) + } + } +} + +// Even if the creator performs type conversion, it is distinguished +// because a speedup due to rtype matching of filterReturnValue can be expected for the specified type. +internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer( + creator: Method, + private val inputType: Class<*>, + converter: ValueClassBoxConverter, +) : WrapsNullableValueClassDeserializer(converter.boxedClass) { + private val handle: MethodHandle init { - ClassUtil.checkAndFixAccess(creator, false) + val unreflect = unreflect(creator).run { + asType(type().changeParameterType(0, ANY_CLASS)) + } + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) } // Cache the result of wrapping null, since the result is always expected to be the same. @@ -108,7 +207,37 @@ internal class WrapsNullableValueClassBoxDeserializer( // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order. // Input is null only when called from KotlinValueInstantiator. @Suppress("UNCHECKED_CAST") - private fun instantiate(input: Any?): D = converter.convert(creator.invoke(null, input) as S) + private fun instantiate(input: Any?): D = handle.invokeExact(input) as D + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { + val input = p.readValueAs(inputType) + return instantiate(input) + } +} + +internal class WrapsAnyValueClassBoxDeserializer( + creator: Method, + private val inputType: Class<*>, + converter: GenericValueClassBoxConverter, +) : WrapsNullableValueClassDeserializer(converter.boxedClass) { + private val handle: MethodHandle + + init { + val unreflect = unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE) + handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle) + } + + // Cache the result of wrapping null, since the result is always expected to be the same. + @get:JvmName("boxedNullValue") + private val boxedNullValue: D by lazy { instantiate(null) } + + override fun getBoxedNullValue(): D = boxedNullValue + + // To instantiate the value class in the same way as other classes, + // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order. + // Input is null only when called from KotlinValueInstantiator. + @Suppress("UNCHECKED_CAST") + private fun instantiate(input: Any?): D = handle.invokeExact(input) as D override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D { val input = p.readValueAs(inputType) @@ -169,7 +298,20 @@ internal class KotlinDeserializers( rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass, cache.getJmClass(rawClass)!!)?.let { val unboxedClass = it.returnType val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass) - WrapsNullableValueClassBoxDeserializer(it, converter) + + when (converter) { + is ValueClassBoxConverter.Specified -> { + val inputType = it.parameterTypes[0] + + if (inputType == unboxedClass) { + NoConversionCreatorBoxDeserializer.create(it, converter) + } else { + HasConversionCreatorWrapsSpecifiedBoxDeserializer(it, inputType, converter) + } + } + is GenericValueClassBoxConverter -> + WrapsAnyValueClassBoxDeserializer(it, it.parameterTypes[0], converter) + } } else -> null } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt index 3a6f93c9..d0803ce3 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/deserializers/KotlinKeyDeserializers.kt @@ -9,15 +9,27 @@ import com.fasterxml.jackson.databind.KeyDeserializer import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializer import com.fasterxml.jackson.databind.exc.InvalidDefinitionException import com.fasterxml.jackson.databind.module.SimpleKeyDeserializers -import com.fasterxml.jackson.databind.util.ClassUtil +import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.INT_CLASS +import io.github.projectmapk.jackson.module.kogera.IntValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.JAVA_UUID_CLASS +import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassBoxConverter +import io.github.projectmapk.jackson.module.kogera.LONG_CLASS +import io.github.projectmapk.jackson.module.kogera.LongValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.STRING_CLASS +import io.github.projectmapk.jackson.module.kogera.StringValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass import io.github.projectmapk.jackson.module.kogera.toSignature +import io.github.projectmapk.jackson.module.kogera.unreflect +import io.github.projectmapk.jackson.module.kogera.unreflectAsType +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.math.BigInteger -import kotlin.metadata.isSecondary -import kotlin.metadata.jvm.signature +import java.util.UUID // The reason why key is treated as nullable is to match the tentative behavior of StdKeyDeserializer. // If StdKeyDeserializer is modified, need to modify this too. @@ -51,34 +63,100 @@ internal object ULongKeyDeserializer : StdKeyDeserializer(-1, ULong::class.java) } // The implementation is designed to be compatible with various creators, just in case. -internal class ValueClassKeyDeserializer( - private val creator: Method, - private val converter: ValueClassBoxConverter, +internal sealed class ValueClassKeyDeserializer( + converter: ValueClassBoxConverter, + creatorHandle: MethodHandle, ) : KeyDeserializer() { - private val unboxedClass: Class<*> = creator.parameterTypes[0] + private val boxedClass: Class = converter.boxedClass - init { - creator.apply { ClassUtil.checkAndFixAccess(this, false) } - } + protected abstract val unboxedClass: Class<*> + protected val handle: MethodHandle = MethodHandles.filterReturnValue(creatorHandle, converter.boxHandle) // Based on databind error // https://github.com/FasterXML/jackson-databind/blob/341f8d360a5f10b5e609d6ee0ea023bf597ce98a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java#L624 private fun errorMessage(boxedType: JavaType): String = "Could not find (Map) Key deserializer for types " + "wrapped in $boxedType" - override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any { + // Since the input to handle must be strict, invoke should be implemented in each class + protected abstract fun invokeExact(value: S): D + + final override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any { val unboxedJavaType = ctxt.constructType(unboxedClass) return try { // findKeyDeserializer does not return null, and an exception will be thrown if not found. val value = ctxt.findKeyDeserializer(unboxedJavaType, null).deserializeKey(key, ctxt) @Suppress("UNCHECKED_CAST") - converter.convert(creator.invoke(null, value) as S) + invokeExact(value as S) } catch (e: InvalidDefinitionException) { - throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(converter.boxedClass)), e) + throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(boxedClass)), e) } } + internal sealed class WrapsSpecified( + converter: ValueClassBoxConverter, + creator: Method, + ) : ValueClassKeyDeserializer( + converter, + // 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), + ) + + internal class WrapsInt( + converter: IntValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = INT_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Int): D = handle.invokeExact(value) as D + } + + internal class WrapsLong( + converter: LongValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = LONG_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: Long): D = handle.invokeExact(value) as D + } + + internal class WrapsString( + converter: StringValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = STRING_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: String?): D = handle.invokeExact(value) as D + } + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassBoxConverter, + creator: Method, + ) : WrapsSpecified(converter, creator) { + override val unboxedClass: Class<*> get() = JAVA_UUID_CLASS + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D + } + + internal class WrapsAny( + converter: GenericValueClassBoxConverter, + creator: Method, + ) : ValueClassKeyDeserializer( + converter, + unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE), + ) { + override val unboxedClass: Class<*> = creator.returnType + + @Suppress("UNCHECKED_CAST") + override fun invokeExact(value: S): D = handle.invokeExact(value) as D + } + companion object { fun createOrNull( valueClass: Class<*>, @@ -97,7 +175,13 @@ internal class ValueClassKeyDeserializer( val converter = cache.getValueClassBoxConverter(unboxedClass, valueClass) - ValueClassKeyDeserializer(it, converter) + when (converter) { + is IntValueClassBoxConverter -> WrapsInt(converter, it) + is LongValueClassBoxConverter -> WrapsLong(converter, it) + is StringValueClassBoxConverter -> WrapsString(converter, it) + is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(converter, it) + is GenericValueClassBoxConverter -> WrapsAny(converter, it) + } } } } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt index cb0dac48..1a275297 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucket.kt @@ -48,7 +48,7 @@ private fun IntArray.update(index: Int, operation: MaskOperation) { internal class BucketGenerator( parameterTypes: List>, valueParameters: List, - private val converters: List?>, + private val converters: List?>, ) { private val valueParameterSize: Int = parameterTypes.size private val originalAbsentArgs: Array @@ -99,7 +99,7 @@ internal class ArgumentBucket( val valueParameterSize: Int, val arguments: Array, val masks: IntArray, - private val converters: List?>, + private val converters: List?>, ) { /** * Sets the argument corresponding to index. diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt index 5618ad29..8c221cb8 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/MethodValueCreator.kt @@ -2,6 +2,7 @@ package io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.crea import com.fasterxml.jackson.databind.util.ClassUtil import io.github.projectmapk.jackson.module.kogera.ANY_CLASS +import io.github.projectmapk.jackson.module.kogera.INT_CLASS import io.github.projectmapk.jackson.module.kogera.ReflectionCache import io.github.projectmapk.jackson.module.kogera.call import io.github.projectmapk.jackson.module.kogera.deser.valueInstantiator.argumentBucket.ArgumentBucket @@ -51,7 +52,7 @@ internal class MethodValueCreator( temp[0] = companionObjectClass // companion object parameterTypes.copyInto(temp, 1) // parameter types for (i in (valueParameterSize + 1)..(valueParameterSize + maskSize)) { // masks - temp[i] = Int::class.java + temp[i] = INT_CLASS } temp[valueParameterSize + maskSize + 1] = ANY_CLASS // maker temp diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt index ebf3d22b..dd45269b 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/creator/ValueCreator.kt @@ -63,8 +63,8 @@ internal sealed class ValueCreator { internal fun List.mapToConverters( rawTypes: List>, cache: ReflectionCache, -): List?> = mapIndexed { i, param -> +): List?> = mapIndexed { i, param -> param.reconstructedClassOrNull ?.takeIf { it.isUnboxableValueClass() && rawTypes[i] != it } ?.let { cache.getValueClassUnboxConverter(it) } -} as List?> // Cast to cheat generics +} as List?> // Cast to cheat generics diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt index 09dbba08..caacdad3 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/Converters.kt @@ -3,10 +3,10 @@ package io.github.projectmapk.jackson.module.kogera.ser import com.fasterxml.jackson.databind.JavaType import com.fasterxml.jackson.databind.type.TypeFactory import com.fasterxml.jackson.databind.util.StdConverter +import io.github.projectmapk.jackson.module.kogera.GenericValueClassBoxConverter import io.github.projectmapk.jackson.module.kogera.JavaDuration import io.github.projectmapk.jackson.module.kogera.KOTLIN_DURATION_CLASS import io.github.projectmapk.jackson.module.kogera.KotlinDuration -import io.github.projectmapk.jackson.module.kogera.ValueClassBoxConverter import kotlin.time.toJavaDuration internal class SequenceToIteratorConverter(private val input: JavaType) : StdConverter, Iterator<*>>() { @@ -21,7 +21,7 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon } internal object KotlinDurationValueToJavaDurationConverter : StdConverter() { - private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KOTLIN_DURATION_CLASS) } + private val boxConverter by lazy { GenericValueClassBoxConverter(Long::class.java, KOTLIN_DURATION_CLASS) } override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value)) } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt index 6ed9c124..e7e7dd26 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinKeySerializers.kt @@ -9,14 +9,28 @@ import com.fasterxml.jackson.databind.SerializationConfig import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.module.SimpleSerializers import com.fasterxml.jackson.databind.ser.std.StdSerializer +import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.GenericValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.INT_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.IntValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.JAVA_UUID_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.LONG_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.LongValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.STRING_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.StringValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass +import io.github.projectmapk.jackson.module.kogera.unreflectAsType +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType import java.lang.reflect.Method import java.lang.reflect.Modifier internal class ValueClassUnboxKeySerializer( - private val converter: ValueClassUnboxConverter, + private val converter: ValueClassUnboxConverter, ) : StdSerializer(converter.valueClass) { override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { val unboxed = converter.convert(value) @@ -31,21 +45,18 @@ internal class ValueClassUnboxKeySerializer( } } -// Class must be UnboxableValueClass. -private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method -> - Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value } -} - -internal class ValueClassStaticJsonKeySerializer( - private val converter: ValueClassUnboxConverter, - private val staticJsonKeyGetter: Method, +internal sealed class ValueClassStaticJsonKeySerializer( + converter: ValueClassUnboxConverter, + staticJsonValueGetter: Method, + methodType: MethodType, ) : StdSerializer(converter.valueClass) { - private val keyType: Class<*> = staticJsonKeyGetter.returnType + private val keyType: Class<*> = staticJsonValueGetter.returnType + private val handle: MethodHandle = unreflectAsType(staticJsonValueGetter, methodType).let { + MethodHandles.filterReturnValue(converter.unboxHandle, it) + } - override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { - val unboxed = converter.convert(value) - // As shown in the processing of the factory function, jsonValueGetter is always a static method. - val jsonKey: Any? = staticJsonKeyGetter.invoke(null, unboxed) + final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val jsonKey: Any? = handle.invokeExact(value) val serializer = jsonKey ?.let { provider.findKeySerializer(keyType, null) } @@ -54,14 +65,76 @@ internal class ValueClassStaticJsonKeySerializer( serializer.serialize(jsonKey, gen, provider) } + internal class WrapsInt( + converter: IntValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + INT_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsLong( + converter: LongValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + LONG_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsString( + converter: StringValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + STRING_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + JAVA_UUID_TO_ANY_METHOD_TYPE, + ) + + internal class WrapsAny( + converter: GenericValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonKeySerializer( + converter, + staticJsonValueGetter, + ANY_TO_ANY_METHOD_TYPE, + + ) + companion object { + // Class must be UnboxableValueClass. + private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method -> + Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value } + } + // `t` must be UnboxableValueClass. // If create a function with a JsonValue in the value class, // it will be compiled as a static method (= cannot be processed properly by Jackson), // so use a ValueClassSerializer.StaticJsonValue to handle this. - fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter.valueClass + fun createOrNull( + converter: ValueClassUnboxConverter, + ): ValueClassStaticJsonKeySerializer? = converter + .valueClass .getStaticJsonKeyGetter() - ?.let { ValueClassStaticJsonKeySerializer(converter, it) } + ?.let { + when (converter) { + is IntValueClassUnboxConverter -> WrapsInt(converter, it) + is LongValueClassUnboxConverter -> WrapsLong(converter, it) + is StringValueClassUnboxConverter -> WrapsString(converter, it) + is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it) + is GenericValueClassUnboxConverter -> WrapsAny(converter, it) + } + } } } diff --git a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt index 90b303fe..811942fe 100644 --- a/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt +++ b/src/main/kotlin/io/github/projectmapk/jackson/module/kogera/ser/serializers/KotlinSerializers.kt @@ -9,9 +9,22 @@ import com.fasterxml.jackson.databind.SerializationConfig import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.module.SimpleSerializers import com.fasterxml.jackson.databind.ser.std.StdSerializer +import io.github.projectmapk.jackson.module.kogera.ANY_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.GenericValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.INT_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.IntValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.JAVA_UUID_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.JavaUuidValueClassUnboxConverter +import io.github.projectmapk.jackson.module.kogera.LONG_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.LongValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ReflectionCache +import io.github.projectmapk.jackson.module.kogera.STRING_TO_ANY_METHOD_TYPE +import io.github.projectmapk.jackson.module.kogera.StringValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.ValueClassUnboxConverter import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass +import io.github.projectmapk.jackson.module.kogera.unreflectAsType +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.lang.reflect.Modifier import java.math.BigInteger @@ -54,26 +67,76 @@ private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods. Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value } } -internal class ValueClassStaticJsonValueSerializer( - private val converter: ValueClassUnboxConverter, - private val staticJsonValueGetter: Method, +internal sealed class ValueClassStaticJsonValueSerializer( + converter: ValueClassUnboxConverter, + staticJsonValueHandle: MethodHandle, ) : StdSerializer(converter.valueClass) { - override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { - val unboxed = converter.convert(value) - // As shown in the processing of the factory function, jsonValueGetter is always a static method. - val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed) + private val handle: MethodHandle = MethodHandles.filterReturnValue(converter.unboxHandle, staticJsonValueHandle) + + final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val jsonValue: Any? = handle.invokeExact(value) provider.defaultSerializeValue(jsonValue, gen) } + internal class WrapsInt( + converter: IntValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsLong( + converter: LongValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsString( + converter: StringValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsJavaUuid( + converter: JavaUuidValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE), + ) + + internal class WrapsAny( + converter: GenericValueClassUnboxConverter, + staticJsonValueGetter: Method, + ) : ValueClassStaticJsonValueSerializer( + converter, + unreflectAsType(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE), + ) + companion object { // `t` must be UnboxableValueClass. // If create a function with a JsonValue in the value class, // it will be compiled as a static method (= cannot be processed properly by Jackson), // so use a ValueClassSerializer.StaticJsonValue to handle this. - fun createOrNull(converter: ValueClassUnboxConverter): StdSerializer? = converter + fun createOrNull( + converter: ValueClassUnboxConverter, + ): ValueClassStaticJsonValueSerializer? = converter .valueClass .getStaticJsonValueGetter() - ?.let { ValueClassStaticJsonValueSerializer(converter, it) } + ?.let { + when (converter) { + is IntValueClassUnboxConverter -> WrapsInt(converter, it) + is LongValueClassUnboxConverter -> WrapsLong(converter, it) + is StringValueClassUnboxConverter -> WrapsString(converter, it) + is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it) + is GenericValueClassUnboxConverter -> WrapsAny(converter, it) + } + } } } diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt index f27e3246..6c220dc4 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/deser/valueInstantiator/argumentBucket/ArgumentBucketTest.kt @@ -110,7 +110,7 @@ private class ArgumentBucketTest { @Test fun unboxTest() { @Suppress("UNCHECKED_CAST") - val converter = ValueClassUnboxConverter(V::class.java) as ValueClassUnboxConverter + val converter = ValueClassUnboxConverter.create(V::class.java) as ValueClassUnboxConverter val generator = BucketGenerator( listOf(Int::class.java, V::class.java), (1..2).map { mockValueParameter() }, diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt index 87aa5ec9..a84e16e5 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/WithoutCustomDeserializeMethodTest.kt @@ -8,7 +8,6 @@ import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import java.lang.reflect.InvocationTargetException class WithoutCustomDeserializeMethodTest { companion object { @@ -132,8 +131,8 @@ class WithoutCustomDeserializeMethodTest { @Test fun callConstructorCheckTest() { - val e = assertThrows { defaultMapper.readValue("-1") } - Assertions.assertTrue(e.cause === throwable) + val e = assertThrows { defaultMapper.readValue("-1") } + Assertions.assertTrue(e === throwable) } // If all JsonCreator tests are OK, no need to check throws from factory functions. diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt index 35c0c7d1..466f59d8 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/deser/valueClass/mapKey/WithoutCustomDeserializeMethodTest.kt @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import java.lang.reflect.InvocationTargetException import com.fasterxml.jackson.databind.KeyDeserializer as JacksonKeyDeserializer class WithoutCustomDeserializeMethodTest { @@ -98,10 +97,10 @@ class WithoutCustomDeserializeMethodTest { @Test fun callConstructorCheckTest() { - val e = assertThrows { + val e = assertThrows { defaultMapper.readValue>("""{"-1":null}""") } - assertTrue(e.cause === throwable) + assertTrue(e === throwable) } data class Wrapped(val first: String, val second: String) { diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/WithoutCustomSerializeMethodTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/WithoutCustomSerializeMethodTest.kt new file mode 100644 index 00000000..8688dc6f --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/WithoutCustomSerializeMethodTest.kt @@ -0,0 +1,173 @@ +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass + +import io.github.projectmapk.jackson.module.kogera.defaultMapper +import io.github.projectmapk.jackson.module.kogera.testPrettyWriter +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class WithoutCustomSerializeMethodTest { + @JvmInline + value class Primitive(val v: Int) + + @JvmInline + value class NonNullObject(val v: String) + + @JvmInline + value class NullableObject(val v: String?) + + @JvmInline + value class NullablePrimitive(val v: Int?) + + @JvmInline + value class TwoUnitPrimitive(val v: Long) + + private val writer = defaultMapper.testPrettyWriter() + + @Nested + inner class DirectSerializeTest { + @Test + fun primitive() { + val result = writer.writeValueAsString(Primitive(1)) + assertEquals("1", result) + } + + @Test + fun nonNullObject() { + val result = writer.writeValueAsString(NonNullObject("foo")) + assertEquals("\"foo\"", result) + } + + @Suppress("ClassName") + @Nested + inner class NullableObject_ { + @Test + fun value() { + val result = writer.writeValueAsString(NullableObject("foo")) + assertEquals("\"foo\"", result) + } + + @Test + fun nullValue() { + val result = writer.writeValueAsString(NullableObject(null)) + assertEquals("null", result) + } + } + + @Suppress("ClassName") + @Nested + inner class NullablePrimitive_ { + @Test + fun value() { + val result = writer.writeValueAsString(NullablePrimitive(1)) + assertEquals("1", result) + } + + @Test + fun nullValue() { + val result = writer.writeValueAsString(NullablePrimitive(null)) + assertEquals("null", result) + } + } + + @Test + fun twoUnitPrimitive() { + val result = writer.writeValueAsString(TwoUnitPrimitive(1)) + assertEquals("1", result) + } + } + + data class Src( + val pNn: Primitive, + val pN: Primitive?, + val nnoNn: NonNullObject, + val nnoN: NonNullObject?, + val noNn: NullableObject, + val noN: NullableObject?, + val npNn: NullablePrimitive, + val npN: NullablePrimitive?, + val tupNn: TwoUnitPrimitive, + val tupN: TwoUnitPrimitive?, + ) + + @Test + fun withoutNull() { + val src = Src( + Primitive(1), + Primitive(2), + NonNullObject("foo"), + NonNullObject("bar"), + NullableObject("baz"), + NullableObject("qux"), + NullablePrimitive(1), + NullablePrimitive(2), + TwoUnitPrimitive(3), + TwoUnitPrimitive(4), + ) + val result = writer.writeValueAsString(src) + + assertEquals( + """ + { + "pNn" : 1, + "pN" : 2, + "nnoNn" : "foo", + "nnoN" : "bar", + "noNn" : "baz", + "noN" : "qux", + "npNn" : 1, + "npN" : 2, + "tupNn" : 3, + "tupN" : 4 + } + """.trimIndent(), + result, + ) + } + + @Test + fun withNull() { + val src = Src( + Primitive(1), + null, + NonNullObject("foo"), + null, + NullableObject(null), + null, + NullablePrimitive(null), + null, + TwoUnitPrimitive(3), + null, + ) + val result = writer.writeValueAsString(src) + + assertEquals( + """ + { + "pNn" : 1, + "pN" : null, + "nnoNn" : "foo", + "nnoN" : null, + "noNn" : null, + "noN" : null, + "npNn" : null, + "npN" : null, + "tupNn" : 3, + "tupN" : null + } + """.trimIndent(), + result, + ) + } + + @JvmInline + value class HasToString(val value: Int) { + override fun toString(): String = "Custom($value)" + } + + @Test + fun toStringTest() { + val result = writer.writeValueAsString(HasToString(42)) + assertEquals("42", result) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/SpecifiedForObjectMapperTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/SpecifiedForObjectMapperTest.kt new file mode 100644 index 00000000..5fddde40 --- /dev/null +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/SpecifiedForObjectMapperTest.kt @@ -0,0 +1,160 @@ +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer + +import com.fasterxml.jackson.databind.module.SimpleModule +import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper +import io.github.projectmapk.jackson.module.kogera.testPrettyWriter +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class SpecifiedForObjectMapperTest { + companion object { + val mapper = jacksonObjectMapper().apply { + val module = SimpleModule().apply { + this.addSerializer(Primitive::class.java, Primitive.Serializer()) + this.addSerializer(NonNullObject::class.java, NonNullObject.Serializer()) + this.addSerializer(NullableObject::class.java, NullableObject.Serializer()) + this.addSerializer(NullablePrimitive::class.java, NullablePrimitive.Serializer()) + this.addSerializer(TwoUnitPrimitive::class.java, TwoUnitPrimitive.Serializer()) + } + this.registerModule(module) + } + val writer = mapper.testPrettyWriter() + } + + @Nested + inner class DirectSerialize { + @Test + fun primitive() { + val result = writer.writeValueAsString(Primitive(1)) + assertEquals("101", result) + } + + @Test + fun nonNullObject() { + val result = writer.writeValueAsString(NonNullObject("foo")) + assertEquals("\"foo-ser\"", result) + } + + @Suppress("ClassName") + @Nested + inner class NullableObject_ { + @Test + fun value() { + val result = writer.writeValueAsString(NullableObject("foo")) + assertEquals("\"foo-ser\"", result) + } + + @Test + fun nullValue() { + val result = writer.writeValueAsString(NullableObject(null)) + assertEquals("\"NULL\"", result) + } + } + + @Suppress("ClassName") + @Nested + inner class NullablePrimitive_ { + @Test + fun value() { + val result = writer.writeValueAsString(NullablePrimitive(1)) + assertEquals("101", result) + } + + @Test + fun nullValue() { + val result = writer.writeValueAsString(NullablePrimitive(null)) + assertEquals("\"NULL\"", result) + } + } + + @Test + fun twoUnitPrimitive() { + val result = writer.writeValueAsString(TwoUnitPrimitive(1)) + assertEquals("101", result) + } + } + + data class Src( + val pNn: Primitive, + val pN: Primitive?, + val nnoNn: NonNullObject, + val nnoN: NonNullObject?, + val noNn: NullableObject, + val noN: NullableObject?, + val npNn: NullablePrimitive, + val npN: NullablePrimitive?, + val tupNn: TwoUnitPrimitive, + val tupN: TwoUnitPrimitive?, + ) + + @Test + fun nonNull() { + val src = Src( + Primitive(1), + Primitive(2), + NonNullObject("foo"), + NonNullObject("bar"), + NullableObject("baz"), + NullableObject("qux"), + NullablePrimitive(3), + NullablePrimitive(4), + TwoUnitPrimitive(5), + TwoUnitPrimitive(6), + ) + val result = writer.writeValueAsString(src) + + assertEquals( + """ + { + "pNn" : 101, + "pN" : 102, + "nnoNn" : "foo-ser", + "nnoN" : "bar-ser", + "noNn" : "baz-ser", + "noN" : "qux-ser", + "npNn" : 103, + "npN" : 104, + "tupNn" : 105, + "tupN" : 106 + } + """.trimIndent(), + result, + ) + } + + @Test + fun withNull() { + val src = Src( + Primitive(1), + null, + NonNullObject("foo"), + null, + NullableObject(null), + null, + NullablePrimitive(null), + null, + TwoUnitPrimitive(5), + null, + ) + val result = writer.writeValueAsString(src) + + assertEquals( + """ + { + "pNn" : 101, + "pN" : null, + "nnoNn" : "foo-ser", + "nnoN" : null, + "noNn" : "NULL", + "noN" : null, + "npNn" : "NULL", + "npN" : null, + "tupNn" : 105, + "tupN" : null + } + """.trimIndent(), + result, + ) + } +} diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NonNullValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NonNullValueTest.kt similarity index 96% rename from src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NonNullValueTest.kt rename to src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NonNullValueTest.kt index 269b6c35..b5b79d53 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NonNullValueTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NonNullValueTest.kt @@ -1,4 +1,4 @@ -package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullableObject.byAnnotation +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullableObject import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NullValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NullValueTest.kt similarity index 96% rename from src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NullValueTest.kt rename to src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NullValueTest.kt index a7ac6a75..1e5c875f 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/byAnnotation/NullValueTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullableObject/NullValueTest.kt @@ -1,4 +1,4 @@ -package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullableObject.byAnnotation +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullableObject import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NonNullValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NonNullValueTest.kt similarity index 96% rename from src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NonNullValueTest.kt rename to src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NonNullValueTest.kt index d6ff8a05..e1410a02 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NonNullValueTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NonNullValueTest.kt @@ -1,4 +1,4 @@ -package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullablePrimitive.byAnnotation +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullablePrimitive import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper diff --git a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NullValueTest.kt b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NullValueTest.kt similarity index 96% rename from src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NullValueTest.kt rename to src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NullValueTest.kt index 5edc4f32..72f1e392 100644 --- a/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/byAnnotation/NullValueTest.kt +++ b/src/test/kotlin/io/github/projectmapk/jackson/module/kogera/zIntegration/ser/valueClass/serializer/byAnnotation/nullablePrimitive/NullValueTest.kt @@ -1,4 +1,4 @@ -package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullablePrimitive.byAnnotation +package io.github.projectmapk.jackson.module.kogera.zIntegration.ser.valueClass.serializer.byAnnotation.nullablePrimitive import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.github.projectmapk.jackson.module.kogera.jacksonObjectMapper