Skip to content

Commit

Permalink
Merge pull request #470 from k163377/github_464_pr3
Browse files Browse the repository at this point in the history
`unbox` `value class` in `Map key` when serializing
  • Loading branch information
dinomite committed Sep 28, 2021
2 parents daf5bdc + e5169e8 commit 80262de
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -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>(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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<*>>(Sequence::class.java) {
Expand Down Expand Up @@ -46,18 +45,12 @@ object ValueClassUnboxSerializer : StdSerializer<Any>(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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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>(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(
Expand All @@ -33,90 +36,90 @@ class Github464 {
val quux: Array<ValueClass?>,
val corge: WrapperClass,
val grault: WrapperClass?,
val garply: Map<ValueClass, ValueClass?>,
val waldo: Map<WrapperClass, WrapperClass?>
val garply: Map<ValueClass, ValueClass?>
)

// 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<Any?>) }
.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>(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<Any?>)
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<ComparisonFailure>("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)
)
}
}
}

Expand All @@ -129,15 +132,22 @@ class Github464 {
gen.writeString(value.value.toString())
}
}
object KeySerializer : StdSerializer<ValueBySerializer>(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.
Expand All @@ -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))
}
}
Expand Down

0 comments on commit 80262de

Please sign in to comment.