diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 468019cd..eba9d00f 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -13,6 +13,10 @@ Authors: Contributors: +wrongwrong (k163377@github) +* #468: Improved support for value classes + (2.13) + Christopher Mason (masoncj@github) * #194: Contributed test case for @JsonIdentityInfo usage (2.12.NEXT) 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 ddad2736..0e1660df 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.ser.Serializers import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.fasterxml.jackson.module.kotlin.ValueClassUnboxSerializer.isUnboxableValueClass import java.math.BigInteger object SequenceSerializer : StdSerializer>(Sequence::class.java) { @@ -40,6 +41,25 @@ 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) + + if (unboxed == null) { + gen.writeNull() + return + } + + provider.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, provider) + } + + // In the future, value class without JvmInline will be available, and unbox may not be able to handle it. + // https://github.com/FasterXML/jackson-module-kotlin/issues/464 + // The JvmInline annotation can be given to Java class, + // so the isKotlinClass decision is necessary (the order is preferable in terms of possible frequency). + fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline } && this.isKotlinClass() +} + @Suppress("EXPERIMENTAL_API_USAGE") internal class KotlinSerializers : Serializers.Base() { override fun findSerializer( @@ -52,6 +72,8 @@ internal class KotlinSerializers : Serializers.Base() { 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 } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github464.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github464.kt new file mode 100644 index 00000000..77996510 --- /dev/null +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github464.kt @@ -0,0 +1,159 @@ +package com.fasterxml.jackson.module.kotlin.test.github + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectWriter +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.test.expectFailure +import org.junit.ComparisonFailure +import org.junit.Ignore +import org.junit.Test +import kotlin.test.assertEquals + +class Github464 { + class UnboxTest { + private val writer: ObjectWriter = jacksonObjectMapper().writerWithDefaultPrettyPrinter() + + @JvmInline + value class ValueClass(val value: Int) + data class WrapperClass(val inlineField: ValueClass) + + class Poko( + val foo: ValueClass, + val bar: ValueClass?, + @JvmField + val baz: ValueClass, + val qux: Collection, + val quux: Array, + val corge: WrapperClass, + val grault: WrapperClass?, + val garply: Map, + val waldo: Map + ) + + // TODO: Remove this function after applying unbox to key of Map and cancel Ignore of test. + @Test + fun tempTest() { + val zeroValue = ValueClass(0) + + val target = Poko( + foo = zeroValue, + bar = null, + baz = zeroValue, + qux = listOf(zeroValue, null), + quux = arrayOf(zeroValue, null), + corge = WrapperClass(zeroValue), + grault = null, + garply = emptyMap(), + waldo = emptyMap() + ) + + assertEquals(""" + { + "foo" : 0, + "bar" : null, + "baz" : 0, + "qux" : [ 0, null ], + "quux" : [ 0, null ], + "corge" : { + "inlineField" : 0 + }, + "grault" : null, + "garply" : { }, + "waldo" : { } + } + """.trimIndent(), + writer.writeValueAsString(target) + ) + } + + @Test + fun test() { + val zeroValue = ValueClass(0) + val oneValue = ValueClass(1) + + val target = Poko( + foo = zeroValue, + bar = null, + baz = zeroValue, + qux = listOf(zeroValue, null), + quux = arrayOf(zeroValue, null), + corge = WrapperClass(zeroValue), + grault = null, + garply = mapOf(zeroValue to zeroValue, oneValue to null), + waldo = mapOf(WrapperClass(zeroValue) to WrapperClass(zeroValue), WrapperClass(oneValue) to null) + ) + + expectFailure("GitHub #469 has been fixed!") { + assertEquals(""" + { + "foo" : 0, + "bar" : null, + "baz" : 0, + "qux" : [ 0, null ], + "quux" : [ 0, null ], + "corge" : { + "inlineField" : 0 + }, + "grault" : null, + "garply" : { + "0" : 0, + "1" : null + }, + "waldo" : { + "{inlineField=0}" : { + "inlineField" : 0 + }, + "{inlineField=1}" : null + } + } + """.trimIndent(), + writer.writeValueAsString(target) + ) + } + } + } + + class SerializerPriorityTest { + @JvmInline + value class ValueBySerializer(val value: Int) + + object Serializer : StdSerializer(ValueBySerializer::class.java) { + override fun serialize(value: ValueBySerializer, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.value.toString()) + } + } + + private val target = listOf(ValueBySerializer(1)) + + @Test + fun simpleTest() { + val sm = SimpleModule().addSerializer(Serializer) + val om: ObjectMapper = jacksonMapperBuilder().addModule(sm).build() + + assertEquals("""["1"]""", om.writeValueAsString(target)) + } + + // Currently, there is a situation where the serialization results are different depending on the registration order of the modules. + // This problem is not addressed because the serializer registered by the user has priority over Extensions.kt, + // since KotlinModule is basically registered first. + @Ignore + @Test + fun priorityTest() { + val sm = SimpleModule().addSerializer(Serializer) + val km = KotlinModule.Builder().build() + val om1: ObjectMapper = JsonMapper.builder().addModules(km, sm).build() + val om2: ObjectMapper = JsonMapper.builder().addModules(sm, km).build() + + // om1(collect) -> """["1"]""" + // om2(broken) -> """[1]""" + assertEquals(om1.writeValueAsString(target), om2.writeValueAsString(target)) + } + } +}