diff --git a/pom.xml b/pom.xml index 91dbace1b..dd4b2f897 100644 --- a/pom.xml +++ b/pom.xml @@ -208,6 +208,13 @@ TODO Remove after release of 2.14 and update the oldVersion above to 2.14. --> com.fasterxml.jackson.module.kotlin.ValueClassBoxSerializer + com.fasterxml.jackson.module.kotlin.ValueClassSerializer$StaticJsonValue + com.fasterxml.jackson.module.kotlin.ValueClassSerializer$Unbox + com.fasterxml.jackson.module.kotlin.ValueClassStaticJsonValueSerializer + com.fasterxml.jackson.module.kotlin.ValueClassUnboxSerializer.serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + com.fasterxml.jackson.databind.jsonschema.SchemaAware[com.fasterxml.jackson.databind.jsonschema.SchemaAware] + com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable[com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable]:INTERFACE_REMOVED, java.io.Serializable[java.io.Serializable] + com.fasterxml.jackson.module.kotlin.ValueClassUnboxSerializer diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt index bb99dd8ef..f674699f3 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.Module import com.fasterxml.jackson.databind.cfg.MapperConfig import com.fasterxml.jackson.databind.introspect.* import com.fasterxml.jackson.databind.jsontype.NamedType +import com.fasterxml.jackson.databind.ser.std.StdSerializer import java.lang.reflect.AccessibleObject import java.lang.reflect.Constructor import java.lang.reflect.Field @@ -62,7 +63,7 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon } // Find a serializer to handle the case where the getter returns an unboxed value from the value class. - override fun findSerializer(am: Annotated): ValueClassBoxSerializer<*>? = when (am) { + override fun findSerializer(am: Annotated): StdSerializer<*>? = when (am) { is AnnotatedMethod -> { val getter = am.member.apply { // If the return value of the getter is a value class, @@ -87,8 +88,10 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon ?.takeIf { it.isValue } ?.java ?.let { outerClazz -> - @Suppress("UNCHECKED_CAST") - ValueClassBoxSerializer(outerClazz, getter.returnType) + val innerClazz = getter.returnType + + ValueClassStaticJsonValueSerializer.createdOrNull(outerClazz, innerClazz) + ?: @Suppress("UNCHECKED_CAST") ValueClassBoxSerializer(outerClazz, innerClazz) } } // Ignore the case of AnnotatedField, because JvmField cannot be set in the field of value class. diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt index 2c27cc7f3..305de370b 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt @@ -1,5 +1,6 @@ package com.fasterxml.jackson.module.kotlin +import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.BeanDescription import com.fasterxml.jackson.databind.JavaType @@ -8,8 +9,9 @@ import com.fasterxml.jackson.databind.SerializationConfig import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.ser.Serializers import com.fasterxml.jackson.databind.ser.std.StdSerializer +import java.lang.reflect.Method +import java.lang.reflect.Modifier import java.math.BigInteger -import kotlin.reflect.KClass object SequenceSerializer : StdSerializer>(Sequence::class.java) { override fun serialize(value: Sequence<*>, gen: JsonGenerator, provider: SerializerProvider) { @@ -43,16 +45,47 @@ object ULongSerializer : StdSerializer(ULong::class.java) { } } -object ValueClassUnboxSerializer : StdSerializer(Any::class.java) { - override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) { - val unboxed = value::class.java.getMethod("unbox-impl").invoke(value) +// Class must be UnboxableValueClass. +private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods + .find { method -> Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue } } - if (unboxed == null) { - provider.findNullValueSerializer(null).serialize(unboxed, gen, provider) - return +internal sealed class ValueClassSerializer(t: Class) : StdSerializer(t) { + object Unbox : ValueClassSerializer(Any::class.java) { + override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) { + val unboxed = value::class.java.getMethod("unbox-impl").invoke(value) + + if (unboxed == null) { + provider.findNullValueSerializer(null).serialize(unboxed, gen, provider) + return + } + + provider.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, provider) + } + } + + class StaticJsonValue( + t: Class, private val staticJsonValueGetter: Method + ) : ValueClassSerializer(t) { + private val unboxMethod: Method = t.getMethod("unbox-impl") + + override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) { + val unboxed = unboxMethod.invoke(value) + // As shown in the processing of the factory function, jsonValueGetter is always a static method. + val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed) + jsonValue + ?.let { provider.findValueSerializer(it::class.java).serialize(it, gen, provider) } + ?: provider.findNullValueSerializer(null).serialize(null, gen, provider) } + } - provider.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, provider) + 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 from(t: Class<*>): ValueClassSerializer<*> = t.getStaticJsonValueGetter() + ?.let { StaticJsonValue(t, it) } + ?: Unbox } } @@ -61,15 +94,19 @@ internal class KotlinSerializers : Serializers.Base() { config: SerializationConfig?, type: JavaType, beanDesc: BeanDescription? - ): JsonSerializer<*>? = when { - Sequence::class.java.isAssignableFrom(type.rawClass) -> SequenceSerializer - UByte::class.java.isAssignableFrom(type.rawClass) -> UByteSerializer - UShort::class.java.isAssignableFrom(type.rawClass) -> UShortSerializer - UInt::class.java.isAssignableFrom(type.rawClass) -> UIntSerializer - ULong::class.java.isAssignableFrom(type.rawClass) -> ULongSerializer - // The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers. - type.rawClass.isUnboxableValueClass() -> ValueClassUnboxSerializer - else -> null + ): JsonSerializer<*>? { + val rawClass = type.rawClass + + return when { + Sequence::class.java.isAssignableFrom(rawClass) -> SequenceSerializer + UByte::class.java.isAssignableFrom(rawClass) -> UByteSerializer + UShort::class.java.isAssignableFrom(rawClass) -> UShortSerializer + UInt::class.java.isAssignableFrom(rawClass) -> UIntSerializer + ULong::class.java.isAssignableFrom(rawClass) -> ULongSerializer + // The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers. + rawClass.isUnboxableValueClass() -> ValueClassSerializer.from(rawClass) + else -> null + } } } @@ -89,3 +126,27 @@ internal class ValueClassBoxSerializer( provider.findValueSerializer(outerClazz).serialize(boxed, gen, provider) } } + +internal class ValueClassStaticJsonValueSerializer private constructor( + innerClazz: Class, + private val staticJsonValueGetter: Method +) : StdSerializer(innerClazz) { + override fun serialize(value: T?, gen: JsonGenerator, provider: SerializerProvider) { + // As shown in the processing of the factory function, jsonValueGetter is always a static method. + val jsonValue: Any? = staticJsonValueGetter.invoke(null, value) + jsonValue + ?.let { provider.findValueSerializer(it::class.java).serialize(it, gen, provider) } + ?: provider.findNullValueSerializer(null).serialize(null, gen, provider) + } + + // Since JsonValue can be processed correctly if it is given to a non-static getter/field, + // this class will only process if it is a `static` method. + companion object { + fun createdOrNull( + outerClazz: Class, + innerClazz: Class + ): ValueClassStaticJsonValueSerializer? = outerClazz + .getStaticJsonValueGetter() + ?.let { ValueClassStaticJsonValueSerializer(innerClazz, it) } + } +} diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/GitHub530.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/GitHub530.kt new file mode 100644 index 000000000..99139ea42 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/GitHub530.kt @@ -0,0 +1,234 @@ +package com.fasterxml.jackson.module.kotlin.test.github + +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder +import kotlin.test.assertEquals +import org.junit.Test + +class GitHub530 { + // At the moment, the output is the same with or without `JsonValue`, + // but this pattern is included in the test case in case the option to default to a serialization method that + // does not `unbox` is introduced in the future. + @JvmInline + value class ValueParamGetterAnnotated(@get:JsonValue val value: Int) + + @JvmInline + value class ValueParamFieldAnnotated(@JvmField @field:JsonValue val value: Int) + + @JvmInline + value class PropertyWithOverriddenGetter(val value: Int) { + @get:JsonValue + val jsonValue: String + get() = this.toString() + } + + @JvmInline + value class DirectlyOverriddenGetter(val value: Int) { + @JsonValue + fun getJsonValue(): String = this.toString() + } + + interface JsonValueGetter { + @get:JsonValue + val jsonValue: String + get() = this.toString() + } + + @JvmInline + value class JsonValueGetterImplementation(val value: Int) : JsonValueGetter + + private val writer = jacksonMapperBuilder().build().writerWithDefaultPrettyPrinter() + + @Test + fun valueParamGetterAnnotated() { + data class Data( + val nonNull: ValueParamGetterAnnotated, + val nullable: ValueParamGetterAnnotated? + ) + + assertEquals( + """ + { + "nonNull" : 0, + "nullable" : 1 + } + """.trimIndent(), + writer.writeValueAsString( + Data( + ValueParamGetterAnnotated(0), + ValueParamGetterAnnotated(1) + ) + ) + ) + } + + @Test + fun valueParamFieldAnnoated() { + data class Data( + val nonNull: ValueParamFieldAnnotated, + val nullable: ValueParamFieldAnnotated? + ) + + assertEquals( + """ + { + "nonNull" : 0, + "nullable" : 1 + } + """.trimIndent(), + writer.writeValueAsString( + Data( + ValueParamFieldAnnotated(0), + ValueParamFieldAnnotated(1) + ) + ) + ) + } + + @Test + fun propertyWithOverriddenGetter() { + data class Data( + val nonNull: PropertyWithOverriddenGetter, + val nullable: PropertyWithOverriddenGetter? + ) + + assertEquals( + """ + { + "nonNull" : "PropertyWithOverriddenGetter(value=0)", + "nullable" : "PropertyWithOverriddenGetter(value=1)" + } + """.trimIndent(), + writer.writeValueAsString( + Data( + PropertyWithOverriddenGetter(0), + PropertyWithOverriddenGetter(1) + ) + ) + ) + } + + @Test + fun directlyOverriddenGetter() { + data class Data( + val nonNull: DirectlyOverriddenGetter, + val nullable: DirectlyOverriddenGetter? + ) + + assertEquals( + """ + { + "nonNull" : "DirectlyOverriddenGetter(value=0)", + "nullable" : "DirectlyOverriddenGetter(value=1)" + } + """.trimIndent(), + writer.writeValueAsString( + Data( + DirectlyOverriddenGetter(0), + DirectlyOverriddenGetter(1) + ) + ) + ) + } + + @Test + fun propertyWithOverriddenGetterAsParameterizedType() { + data class Data( + val nonNull: T, + val nullable: T? + ) + + assertEquals( + """ + { + "nonNull" : "PropertyWithOverriddenGetter(value=0)", + "nullable" : "PropertyWithOverriddenGetter(value=1)" + } + """.trimIndent(), + writer.writeValueAsString( + Data( + PropertyWithOverriddenGetter(0), + PropertyWithOverriddenGetter(1) + ) + ) + ) + } + + @Test + fun jsonValueGetterImplementationAsConcreteType() { + data class Data( + val nonNull: JsonValueGetterImplementation, + val nullable: JsonValueGetterImplementation? + ) + + assertEquals( + """ + { + "nonNull" : "JsonValueGetterImplementation(value=0)", + "nullable" : "JsonValueGetterImplementation(value=1)" + } + """.trimIndent(), + writer.writeValueAsString( + Data( + JsonValueGetterImplementation(0), + JsonValueGetterImplementation(1) + ) + ) + ) + } + + @Test + fun jsonValueGetterImplementationAsGenericType() { + data class Data( + val nonNull: JsonValueGetter, + val nullable: JsonValueGetter? + ) + + assertEquals( + """ + { + "nonNull" : "JsonValueGetterImplementation(value=0)", + "nullable" : "JsonValueGetterImplementation(value=1)" + } + """.trimIndent(), + writer.writeValueAsString( + Data( + JsonValueGetterImplementation(0), + JsonValueGetterImplementation(1) + ) + ) + ) + } + + @Test + fun inCollection() { + assertEquals( + "[ 0, 1, \"PropertyWithOverriddenGetter(value=2)\", \"DirectlyOverriddenGetter(value=3)\", \"JsonValueGetterImplementation(value=4)\" ]", + writer.writeValueAsString( + listOf( + ValueParamGetterAnnotated(0), + ValueParamFieldAnnotated(1), + PropertyWithOverriddenGetter(2), + DirectlyOverriddenGetter(3), + JsonValueGetterImplementation(4) + ) + ) + ) + } + + @Test + fun inArray() { + assertEquals( + "[ 0, 1, \"PropertyWithOverriddenGetter(value=2)\", \"DirectlyOverriddenGetter(value=3)\", \"JsonValueGetterImplementation(value=4)\" ]", + writer.writeValueAsString( + arrayOf( + ValueParamGetterAnnotated(0), + ValueParamFieldAnnotated(1), + PropertyWithOverriddenGetter(2), + DirectlyOverriddenGetter(3), + JsonValueGetterImplementation(4) + ) + ) + ) + } +}