Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unbox value class in Collection etc. when serializing #468

Merged
merged 6 commits into from
Jun 23, 2021
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
4 changes: 4 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<*>>(Sequence::class.java) {
Expand Down Expand Up @@ -40,6 +41,25 @@ object ULongSerializer : StdSerializer<ULong>(ULong::class.java) {
}
}

object ValueClassUnboxSerializer : StdSerializer<Any>(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(
Expand All @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ValueClass?>,
val quux: Array<ValueClass?>,
val corge: WrapperClass,
val grault: WrapperClass?,
val garply: Map<ValueClass, ValueClass?>,
val waldo: Map<WrapperClass, WrapperClass?>
)

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

class SerializerPriorityTest {
@JvmInline
value class ValueBySerializer(val value: Int)

object Serializer : StdSerializer<ValueBySerializer>(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))
}
}
}