Skip to content

Commit

Permalink
Merge pull request #468 from k163377/github_464_pr
Browse files Browse the repository at this point in the history
`unbox` `value class` in `Collection` etc. when serializing
  • Loading branch information
dinomite authored Jun 23, 2021
2 parents 0d91f0c + bf79fda commit 3a29578
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 1 deletion.
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))
}
}
}

0 comments on commit 3a29578

Please sign in to comment.