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)
+ )
+ )
+ )
+ }
+}