Skip to content

Commit 3a29578

Browse files
authored
Merge pull request #468 from k163377/github_464_pr
`unbox` `value class` in `Collection` etc. when serializing
2 parents 0d91f0c + bf79fda commit 3a29578

File tree

3 files changed

+186
-1
lines changed

3 files changed

+186
-1
lines changed

release-notes/CREDITS-2.x

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Authors:
1313

1414
Contributors:
1515

16+
wrongwrong (k163377@github)
17+
* #468: Improved support for value classes
18+
(2.13)
19+
1620
Christopher Mason (masoncj@github)
1721
* #194: Contributed test case for @JsonIdentityInfo usage
1822
(2.12.NEXT)

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonGenerator
66
import com.fasterxml.jackson.databind.*
77
import com.fasterxml.jackson.databind.ser.Serializers
88
import com.fasterxml.jackson.databind.ser.std.StdSerializer
9+
import com.fasterxml.jackson.module.kotlin.ValueClassUnboxSerializer.isUnboxableValueClass
910
import java.math.BigInteger
1011

1112
object SequenceSerializer : StdSerializer<Sequence<*>>(Sequence::class.java) {
@@ -40,6 +41,25 @@ object ULongSerializer : StdSerializer<ULong>(ULong::class.java) {
4041
}
4142
}
4243

44+
object ValueClassUnboxSerializer : StdSerializer<Any>(Any::class.java) {
45+
override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
46+
val unboxed = value::class.java.getMethod("unbox-impl").invoke(value)
47+
48+
if (unboxed == null) {
49+
gen.writeNull()
50+
return
51+
}
52+
53+
provider.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, provider)
54+
}
55+
56+
// In the future, value class without JvmInline will be available, and unbox may not be able to handle it.
57+
// https://github.com/FasterXML/jackson-module-kotlin/issues/464
58+
// The JvmInline annotation can be given to Java class,
59+
// so the isKotlinClass decision is necessary (the order is preferable in terms of possible frequency).
60+
fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline } && this.isKotlinClass()
61+
}
62+
4363
@Suppress("EXPERIMENTAL_API_USAGE")
4464
internal class KotlinSerializers : Serializers.Base() {
4565
override fun findSerializer(
@@ -52,6 +72,8 @@ internal class KotlinSerializers : Serializers.Base() {
5272
UShort::class.java.isAssignableFrom(type.rawClass) -> UShortSerializer
5373
UInt::class.java.isAssignableFrom(type.rawClass) -> UIntSerializer
5474
ULong::class.java.isAssignableFrom(type.rawClass) -> ULongSerializer
75+
// The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers.
76+
type.rawClass.isUnboxableValueClass() -> ValueClassUnboxSerializer
5577
else -> null
5678
}
57-
}
79+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package com.fasterxml.jackson.module.kotlin.test.github
2+
3+
import com.fasterxml.jackson.core.JsonGenerator
4+
import com.fasterxml.jackson.databind.ObjectMapper
5+
import com.fasterxml.jackson.databind.ObjectWriter
6+
import com.fasterxml.jackson.databind.SerializerProvider
7+
import com.fasterxml.jackson.databind.json.JsonMapper
8+
import com.fasterxml.jackson.databind.module.SimpleModule
9+
import com.fasterxml.jackson.databind.ser.std.StdSerializer
10+
import com.fasterxml.jackson.module.kotlin.KotlinModule
11+
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
12+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
13+
import com.fasterxml.jackson.module.kotlin.test.expectFailure
14+
import org.junit.ComparisonFailure
15+
import org.junit.Ignore
16+
import org.junit.Test
17+
import kotlin.test.assertEquals
18+
19+
class Github464 {
20+
class UnboxTest {
21+
private val writer: ObjectWriter = jacksonObjectMapper().writerWithDefaultPrettyPrinter()
22+
23+
@JvmInline
24+
value class ValueClass(val value: Int)
25+
data class WrapperClass(val inlineField: ValueClass)
26+
27+
class Poko(
28+
val foo: ValueClass,
29+
val bar: ValueClass?,
30+
@JvmField
31+
val baz: ValueClass,
32+
val qux: Collection<ValueClass?>,
33+
val quux: Array<ValueClass?>,
34+
val corge: WrapperClass,
35+
val grault: WrapperClass?,
36+
val garply: Map<ValueClass, ValueClass?>,
37+
val waldo: Map<WrapperClass, WrapperClass?>
38+
)
39+
40+
// TODO: Remove this function after applying unbox to key of Map and cancel Ignore of test.
41+
@Test
42+
fun tempTest() {
43+
val zeroValue = ValueClass(0)
44+
45+
val target = Poko(
46+
foo = zeroValue,
47+
bar = null,
48+
baz = zeroValue,
49+
qux = listOf(zeroValue, null),
50+
quux = arrayOf(zeroValue, null),
51+
corge = WrapperClass(zeroValue),
52+
grault = null,
53+
garply = emptyMap(),
54+
waldo = emptyMap()
55+
)
56+
57+
assertEquals("""
58+
{
59+
"foo" : 0,
60+
"bar" : null,
61+
"baz" : 0,
62+
"qux" : [ 0, null ],
63+
"quux" : [ 0, null ],
64+
"corge" : {
65+
"inlineField" : 0
66+
},
67+
"grault" : null,
68+
"garply" : { },
69+
"waldo" : { }
70+
}
71+
""".trimIndent(),
72+
writer.writeValueAsString(target)
73+
)
74+
}
75+
76+
@Test
77+
fun test() {
78+
val zeroValue = ValueClass(0)
79+
val oneValue = ValueClass(1)
80+
81+
val target = Poko(
82+
foo = zeroValue,
83+
bar = null,
84+
baz = zeroValue,
85+
qux = listOf(zeroValue, null),
86+
quux = arrayOf(zeroValue, null),
87+
corge = WrapperClass(zeroValue),
88+
grault = null,
89+
garply = mapOf(zeroValue to zeroValue, oneValue to null),
90+
waldo = mapOf(WrapperClass(zeroValue) to WrapperClass(zeroValue), WrapperClass(oneValue) to null)
91+
)
92+
93+
expectFailure<ComparisonFailure>("GitHub #469 has been fixed!") {
94+
assertEquals("""
95+
{
96+
"foo" : 0,
97+
"bar" : null,
98+
"baz" : 0,
99+
"qux" : [ 0, null ],
100+
"quux" : [ 0, null ],
101+
"corge" : {
102+
"inlineField" : 0
103+
},
104+
"grault" : null,
105+
"garply" : {
106+
"0" : 0,
107+
"1" : null
108+
},
109+
"waldo" : {
110+
"{inlineField=0}" : {
111+
"inlineField" : 0
112+
},
113+
"{inlineField=1}" : null
114+
}
115+
}
116+
""".trimIndent(),
117+
writer.writeValueAsString(target)
118+
)
119+
}
120+
}
121+
}
122+
123+
class SerializerPriorityTest {
124+
@JvmInline
125+
value class ValueBySerializer(val value: Int)
126+
127+
object Serializer : StdSerializer<ValueBySerializer>(ValueBySerializer::class.java) {
128+
override fun serialize(value: ValueBySerializer, gen: JsonGenerator, provider: SerializerProvider) {
129+
gen.writeString(value.value.toString())
130+
}
131+
}
132+
133+
private val target = listOf(ValueBySerializer(1))
134+
135+
@Test
136+
fun simpleTest() {
137+
val sm = SimpleModule().addSerializer(Serializer)
138+
val om: ObjectMapper = jacksonMapperBuilder().addModule(sm).build()
139+
140+
assertEquals("""["1"]""", om.writeValueAsString(target))
141+
}
142+
143+
// Currently, there is a situation where the serialization results are different depending on the registration order of the modules.
144+
// This problem is not addressed because the serializer registered by the user has priority over Extensions.kt,
145+
// since KotlinModule is basically registered first.
146+
@Ignore
147+
@Test
148+
fun priorityTest() {
149+
val sm = SimpleModule().addSerializer(Serializer)
150+
val km = KotlinModule.Builder().build()
151+
val om1: ObjectMapper = JsonMapper.builder().addModules(km, sm).build()
152+
val om2: ObjectMapper = JsonMapper.builder().addModules(sm, km).build()
153+
154+
// om1(collect) -> """["1"]"""
155+
// om2(broken) -> """[1]"""
156+
assertEquals(om1.writeValueAsString(target), om2.writeValueAsString(target))
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)