Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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