diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Extensions.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Extensions.kt index 730be8524..8e283c0e5 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Extensions.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Extensions.kt @@ -114,3 +114,9 @@ internal fun Int.toBitSet(): BitSet { } return bits } + +// In the future, value classes without @JvmInline will be available, and unboxing may not be able to handle it. +// https://github.com/FasterXML/jackson-module-kotlin/issues/464 +// The JvmInline annotation can be added to Java classes, +// so the isKotlinClass decision is necessary (the order is preferable in terms of possible frequency). +internal fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline } && this.isKotlinClass() diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt new file mode 100644 index 000000000..2db5694fc --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt @@ -0,0 +1,32 @@ +package com.fasterxml.jackson.module.kotlin + +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 + +internal object ValueClassUnboxKeySerializer : StdSerializer(Any::class.java) { + override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) { + val method = value::class.java.getMethod("unbox-impl") + val unboxed = method.invoke(value) + + if (unboxed == null) { + val javaType = provider.typeFactory.constructType(method.genericReturnType) + provider.findNullKeySerializer(javaType, null).serialize(null, gen, provider) + return + } + + provider.findKeySerializer(unboxed::class.java, null).serialize(unboxed, gen, provider) + } +} + +internal class KotlinKeySerializers : Serializers.Base() { + override fun findSerializer( + config: SerializationConfig, + type: JavaType, + beanDesc: BeanDescription + ): JsonSerializer<*>? = when { + type.rawClass.isUnboxableValueClass() -> ValueClassUnboxKeySerializer + else -> null + } +} diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt index 4a9f54d99..176453b1a 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -123,6 +123,7 @@ class KotlinModule @Deprecated( context.addDeserializers(KotlinDeserializers()) context.addSerializers(KotlinSerializers()) + context.addKeySerializers(KotlinKeySerializers()) fun addMixIn(clazz: Class<*>, mixin: Class<*>) { context.setMixInAnnotations(clazz, mixin) 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 0e1660df0..7168217cd 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt @@ -6,7 +6,6 @@ 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) { @@ -46,18 +45,12 @@ object ValueClassUnboxSerializer : StdSerializer(Any::class.java) { val unboxed = value::class.java.getMethod("unbox-impl").invoke(value) if (unboxed == null) { - gen.writeNull() + provider.findNullValueSerializer(null).serialize(unboxed, gen, provider) 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") 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 index 779965109..551df6caf 100644 --- 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 @@ -1,6 +1,7 @@ package com.fasterxml.jackson.module.kotlin.test.github import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectWriter import com.fasterxml.jackson.databind.SerializerProvider @@ -10,18 +11,20 @@ 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() + object NullValueClassKeySerializer : StdSerializer(ValueClass::class.java) { + override fun serialize(value: ValueClass?, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeFieldName("null-key") + } + } @JvmInline - value class ValueClass(val value: Int) + value class ValueClass(val value: Int?) data class WrapperClass(val inlineField: ValueClass) class Poko( @@ -33,90 +36,90 @@ class Github464 { val quux: Array, val corge: WrapperClass, val grault: WrapperClass?, - val garply: Map, - val waldo: Map + val garply: 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() - ) + private val zeroValue = ValueClass(0) + private val oneValue = ValueClass(1) + private val nullValue = ValueClass(null) + + private 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, nullValue to nullValue) + ) - assertEquals(""" - { - "foo" : 0, - "bar" : null, - "baz" : 0, - "qux" : [ 0, null ], - "quux" : [ 0, null ], - "corge" : { - "inlineField" : 0 - }, - "grault" : null, - "garply" : { }, - "waldo" : { } - } - """.trimIndent(), + @Test + fun test() { + @Suppress("UNCHECKED_CAST") + val writer: ObjectWriter = jacksonObjectMapper() + .apply { serializerProvider.setNullKeySerializer(NullValueClassKeySerializer as JsonSerializer) } + .writerWithDefaultPrettyPrinter() + + assertEquals( + """ + { + "foo" : 0, + "bar" : null, + "baz" : 0, + "qux" : [ 0, null ], + "quux" : [ 0, null ], + "corge" : { + "inlineField" : 0 + }, + "grault" : null, + "garply" : { + "0" : 0, + "1" : null, + "null-key" : null + } + } + """.trimIndent(), writer.writeValueAsString(target) ) } + object NullValueSerializer : StdSerializer(Any::class.java) { + override fun serialize(value: Any?, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString("null-value") + } + } + @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) + fun nullValueSerializerTest() { + @Suppress("UNCHECKED_CAST") + val writer = jacksonObjectMapper() + .apply { + serializerProvider.setNullKeySerializer(NullValueClassKeySerializer as JsonSerializer) + serializerProvider.setNullValueSerializer(NullValueSerializer) + }.writerWithDefaultPrettyPrinter() + + assertEquals( + """ + { + "foo" : 0, + "bar" : "null-value", + "baz" : 0, + "qux" : [ 0, "null-value" ], + "quux" : [ 0, "null-value" ], + "corge" : { + "inlineField" : 0 + }, + "grault" : "null-value", + "garply" : { + "0" : 0, + "1" : "null-value", + "null-key" : "null-value" + } + } + """.trimIndent(), + writer.writeValueAsString(target) ) - - 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) - ) - } } } @@ -129,15 +132,22 @@ class Github464 { gen.writeString(value.value.toString()) } } + object KeySerializer : StdSerializer(ValueBySerializer::class.java) { + override fun serialize(value: ValueBySerializer, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeFieldName(value.value.toString()) + } + } - private val target = listOf(ValueBySerializer(1)) + private val target = mapOf(ValueBySerializer(1) to ValueBySerializer(2)) + private val sm = SimpleModule() + .addSerializer(Serializer) + .addKeySerializer(ValueBySerializer::class.java, KeySerializer) @Test fun simpleTest() { - val sm = SimpleModule().addSerializer(Serializer) val om: ObjectMapper = jacksonMapperBuilder().addModule(sm).build() - assertEquals("""["1"]""", om.writeValueAsString(target)) + assertEquals("""{"1":"2"}""", om.writeValueAsString(target)) } // Currently, there is a situation where the serialization results are different depending on the registration order of the modules. @@ -146,13 +156,12 @@ class Github464 { @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]""" + // om1(collect) -> """{"1":"2"}""" + // om2(broken) -> """{"1":2}""" assertEquals(om1.writeValueAsString(target), om2.writeValueAsString(target)) } }