Skip to content

Commit 80262de

Browse files
authored
Merge pull request #470 from k163377/github_464_pr3
`unbox` `value class` in `Map key` when serializing
2 parents daf5bdc + e5169e8 commit 80262de

File tree

5 files changed

+134
-93
lines changed

5 files changed

+134
-93
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,9 @@ internal fun Int.toBitSet(): BitSet {
114114
}
115115
return bits
116116
}
117+
118+
// In the future, value classes without @JvmInline will be available, and unboxing may not be able to handle it.
119+
// https://github.com/FasterXML/jackson-module-kotlin/issues/464
120+
// The JvmInline annotation can be added to Java classes,
121+
// so the isKotlinClass decision is necessary (the order is preferable in terms of possible frequency).
122+
internal fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline } && this.isKotlinClass()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.fasterxml.jackson.module.kotlin
2+
3+
import com.fasterxml.jackson.core.JsonGenerator
4+
import com.fasterxml.jackson.databind.*
5+
import com.fasterxml.jackson.databind.ser.Serializers
6+
import com.fasterxml.jackson.databind.ser.std.StdSerializer
7+
8+
internal object ValueClassUnboxKeySerializer : StdSerializer<Any>(Any::class.java) {
9+
override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
10+
val method = value::class.java.getMethod("unbox-impl")
11+
val unboxed = method.invoke(value)
12+
13+
if (unboxed == null) {
14+
val javaType = provider.typeFactory.constructType(method.genericReturnType)
15+
provider.findNullKeySerializer(javaType, null).serialize(null, gen, provider)
16+
return
17+
}
18+
19+
provider.findKeySerializer(unboxed::class.java, null).serialize(unboxed, gen, provider)
20+
}
21+
}
22+
23+
internal class KotlinKeySerializers : Serializers.Base() {
24+
override fun findSerializer(
25+
config: SerializationConfig,
26+
type: JavaType,
27+
beanDesc: BeanDescription
28+
): JsonSerializer<*>? = when {
29+
type.rawClass.isUnboxableValueClass() -> ValueClassUnboxKeySerializer
30+
else -> null
31+
}
32+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class KotlinModule @Deprecated(
123123

124124
context.addDeserializers(KotlinDeserializers())
125125
context.addSerializers(KotlinSerializers())
126+
context.addKeySerializers(KotlinKeySerializers())
126127

127128
fun addMixIn(clazz: Class<*>, mixin: Class<*>) {
128129
context.setMixInAnnotations(clazz, mixin)

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ 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
109
import java.math.BigInteger
1110

1211
object SequenceSerializer : StdSerializer<Sequence<*>>(Sequence::class.java) {
@@ -46,18 +45,12 @@ object ValueClassUnboxSerializer : StdSerializer<Any>(Any::class.java) {
4645
val unboxed = value::class.java.getMethod("unbox-impl").invoke(value)
4746

4847
if (unboxed == null) {
49-
gen.writeNull()
48+
provider.findNullValueSerializer(null).serialize(unboxed, gen, provider)
5049
return
5150
}
5251

5352
provider.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, provider)
5453
}
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()
6154
}
6255

6356
@Suppress("EXPERIMENTAL_API_USAGE")

src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github464.kt

Lines changed: 94 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.fasterxml.jackson.module.kotlin.test.github
22

33
import com.fasterxml.jackson.core.JsonGenerator
4+
import com.fasterxml.jackson.databind.JsonSerializer
45
import com.fasterxml.jackson.databind.ObjectMapper
56
import com.fasterxml.jackson.databind.ObjectWriter
67
import com.fasterxml.jackson.databind.SerializerProvider
@@ -10,18 +11,20 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer
1011
import com.fasterxml.jackson.module.kotlin.KotlinModule
1112
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
1213
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
13-
import com.fasterxml.jackson.module.kotlin.test.expectFailure
14-
import org.junit.ComparisonFailure
1514
import org.junit.Ignore
1615
import org.junit.Test
1716
import kotlin.test.assertEquals
1817

1918
class Github464 {
2019
class UnboxTest {
21-
private val writer: ObjectWriter = jacksonObjectMapper().writerWithDefaultPrettyPrinter()
20+
object NullValueClassKeySerializer : StdSerializer<ValueClass>(ValueClass::class.java) {
21+
override fun serialize(value: ValueClass?, gen: JsonGenerator, provider: SerializerProvider) {
22+
gen.writeFieldName("null-key")
23+
}
24+
}
2225

2326
@JvmInline
24-
value class ValueClass(val value: Int)
27+
value class ValueClass(val value: Int?)
2528
data class WrapperClass(val inlineField: ValueClass)
2629

2730
class Poko(
@@ -33,90 +36,90 @@ class Github464 {
3336
val quux: Array<ValueClass?>,
3437
val corge: WrapperClass,
3538
val grault: WrapperClass?,
36-
val garply: Map<ValueClass, ValueClass?>,
37-
val waldo: Map<WrapperClass, WrapperClass?>
39+
val garply: Map<ValueClass, ValueClass?>
3840
)
3941

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-
)
42+
private val zeroValue = ValueClass(0)
43+
private val oneValue = ValueClass(1)
44+
private val nullValue = ValueClass(null)
45+
46+
private val target = Poko(
47+
foo = zeroValue,
48+
bar = null,
49+
baz = zeroValue,
50+
qux = listOf(zeroValue, null),
51+
quux = arrayOf(zeroValue, null),
52+
corge = WrapperClass(zeroValue),
53+
grault = null,
54+
garply = mapOf(zeroValue to zeroValue, oneValue to null, nullValue to nullValue)
55+
)
5656

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(),
57+
@Test
58+
fun test() {
59+
@Suppress("UNCHECKED_CAST")
60+
val writer: ObjectWriter = jacksonObjectMapper()
61+
.apply { serializerProvider.setNullKeySerializer(NullValueClassKeySerializer as JsonSerializer<Any?>) }
62+
.writerWithDefaultPrettyPrinter()
63+
64+
assertEquals(
65+
"""
66+
{
67+
"foo" : 0,
68+
"bar" : null,
69+
"baz" : 0,
70+
"qux" : [ 0, null ],
71+
"quux" : [ 0, null ],
72+
"corge" : {
73+
"inlineField" : 0
74+
},
75+
"grault" : null,
76+
"garply" : {
77+
"0" : 0,
78+
"1" : null,
79+
"null-key" : null
80+
}
81+
}
82+
""".trimIndent(),
7283
writer.writeValueAsString(target)
7384
)
7485
}
7586

87+
object NullValueSerializer : StdSerializer<Any>(Any::class.java) {
88+
override fun serialize(value: Any?, gen: JsonGenerator, provider: SerializerProvider) {
89+
gen.writeString("null-value")
90+
}
91+
}
92+
7693
@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)
94+
fun nullValueSerializerTest() {
95+
@Suppress("UNCHECKED_CAST")
96+
val writer = jacksonObjectMapper()
97+
.apply {
98+
serializerProvider.setNullKeySerializer(NullValueClassKeySerializer as JsonSerializer<Any?>)
99+
serializerProvider.setNullValueSerializer(NullValueSerializer)
100+
}.writerWithDefaultPrettyPrinter()
101+
102+
assertEquals(
103+
"""
104+
{
105+
"foo" : 0,
106+
"bar" : "null-value",
107+
"baz" : 0,
108+
"qux" : [ 0, "null-value" ],
109+
"quux" : [ 0, "null-value" ],
110+
"corge" : {
111+
"inlineField" : 0
112+
},
113+
"grault" : "null-value",
114+
"garply" : {
115+
"0" : 0,
116+
"1" : "null-value",
117+
"null-key" : "null-value"
118+
}
119+
}
120+
""".trimIndent(),
121+
writer.writeValueAsString(target)
91122
)
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-
}
120123
}
121124
}
122125

@@ -129,15 +132,22 @@ class Github464 {
129132
gen.writeString(value.value.toString())
130133
}
131134
}
135+
object KeySerializer : StdSerializer<ValueBySerializer>(ValueBySerializer::class.java) {
136+
override fun serialize(value: ValueBySerializer, gen: JsonGenerator, provider: SerializerProvider) {
137+
gen.writeFieldName(value.value.toString())
138+
}
139+
}
132140

133-
private val target = listOf(ValueBySerializer(1))
141+
private val target = mapOf(ValueBySerializer(1) to ValueBySerializer(2))
142+
private val sm = SimpleModule()
143+
.addSerializer(Serializer)
144+
.addKeySerializer(ValueBySerializer::class.java, KeySerializer)
134145

135146
@Test
136147
fun simpleTest() {
137-
val sm = SimpleModule().addSerializer(Serializer)
138148
val om: ObjectMapper = jacksonMapperBuilder().addModule(sm).build()
139149

140-
assertEquals("""["1"]""", om.writeValueAsString(target))
150+
assertEquals("""{"1":"2"}""", om.writeValueAsString(target))
141151
}
142152

143153
// Currently, there is a situation where the serialization results are different depending on the registration order of the modules.
@@ -146,13 +156,12 @@ class Github464 {
146156
@Ignore
147157
@Test
148158
fun priorityTest() {
149-
val sm = SimpleModule().addSerializer(Serializer)
150159
val km = KotlinModule.Builder().build()
151160
val om1: ObjectMapper = JsonMapper.builder().addModules(km, sm).build()
152161
val om2: ObjectMapper = JsonMapper.builder().addModules(sm, km).build()
153162

154-
// om1(collect) -> """["1"]"""
155-
// om2(broken) -> """[1]"""
163+
// om1(collect) -> """{"1":"2"}"""
164+
// om2(broken) -> """{"1":2}"""
156165
assertEquals(om1.writeValueAsString(target), om2.writeValueAsString(target))
157166
}
158167
}

0 commit comments

Comments
 (0)