Skip to content

Commit eb7a12a

Browse files
authored
Merge pull request #527 from k163377/github-524/fix
Improvements to serialization of `value class`.
2 parents 6748825 + 1660b94 commit eb7a12a

File tree

4 files changed

+133
-0
lines changed

4 files changed

+133
-0
lines changed

release-notes/CREDITS-2.x

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ wrongwrong (k163377@github)
2424
* #456: Refactor KNAI.findImplicitPropertyName()
2525
* #449: Refactor AnnotatedMethod.hasRequiredMarker()
2626
* #521: Fixed lookup of instantiators
27+
* #527: Improvements to serialization of `value class`.
2728

2829
Dmitri Domanine (novtor@github)
2930
* Contributed fix for #490: Missing value of type JsonNode? is deserialized as NullNode instead of null

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

+39
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import java.lang.reflect.AccessibleObject
1111
import java.lang.reflect.Constructor
1212
import java.lang.reflect.Field
1313
import java.lang.reflect.Method
14+
import kotlin.reflect.KClass
1415
import kotlin.reflect.KFunction
1516
import kotlin.reflect.KMutableProperty1
1617
import kotlin.reflect.KProperty1
1718
import kotlin.reflect.KType
1819
import kotlin.reflect.full.createType
1920
import kotlin.reflect.full.declaredMemberProperties
21+
import kotlin.reflect.full.memberProperties
2022
import kotlin.reflect.jvm.*
2123

2224

@@ -59,6 +61,43 @@ internal class KotlinAnnotationIntrospector(private val context: Module.SetupCon
5961
return super.findCreatorAnnotation(config, a)
6062
}
6163

64+
// Find a serializer to handle the case where the getter returns an unboxed value from the value class.
65+
override fun findSerializer(am: Annotated): ValueClassBoxSerializer<*>? = when (am) {
66+
is AnnotatedMethod -> {
67+
val getter = am.member.apply {
68+
// If the return value of the getter is a value class,
69+
// it will be serialized properly without doing anything.
70+
if (this.returnType.isUnboxableValueClass()) return null
71+
}
72+
73+
val kotlinProperty = getter
74+
.declaringClass
75+
.kotlin
76+
.let {
77+
// KotlinReflectionInternalError is raised in GitHub167 test,
78+
// but it looks like an edge case, so it is ignored.
79+
try {
80+
it.memberProperties
81+
} catch (e: Error) {
82+
null
83+
}
84+
}?.find { it.javaGetter == getter }
85+
86+
(kotlinProperty?.returnType?.classifier as? KClass<*>)
87+
?.takeIf { it.isValue }
88+
?.java
89+
?.let { outerClazz ->
90+
@Suppress("UNCHECKED_CAST")
91+
ValueClassBoxSerializer(outerClazz, getter.returnType)
92+
}
93+
}
94+
// Ignore the case of AnnotatedField, because JvmField cannot be set in the field of value class.
95+
else -> null
96+
}
97+
98+
// Perform proper serialization even if the value wrapped by the value class is null.
99+
override fun findNullSerializer(am: Annotated) = findSerializer(am)
100+
62101
/**
63102
* Subclasses can be detected automatically for sealed classes, since all possible subclasses are known
64103
* at compile-time to Kotlin. This makes [com.fasterxml.jackson.annotation.JsonSubTypes] redundant.

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

+18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.SerializerProvider
99
import com.fasterxml.jackson.databind.ser.Serializers
1010
import com.fasterxml.jackson.databind.ser.std.StdSerializer
1111
import java.math.BigInteger
12+
import kotlin.reflect.KClass
1213

1314
object SequenceSerializer : StdSerializer<Sequence<*>>(Sequence::class.java) {
1415
override fun serialize(value: Sequence<*>, gen: JsonGenerator, provider: SerializerProvider) {
@@ -71,3 +72,20 @@ internal class KotlinSerializers : Serializers.Base() {
7172
else -> null
7273
}
7374
}
75+
76+
// This serializer is used to properly serialize the value class.
77+
// The getter generated for the value class is special,
78+
// so this class will not work properly when added to the Serializers
79+
// (it is configured from KotlinAnnotationIntrospector.findSerializer).
80+
internal class ValueClassBoxSerializer<T : Any>(
81+
private val outerClazz: Class<out Any>, innerClazz: Class<T>
82+
) : StdSerializer<T>(innerClazz) {
83+
private val boxMethod = outerClazz.getMethod("box-impl", innerClazz)
84+
85+
override fun serialize(value: T?, gen: JsonGenerator, provider: SerializerProvider) {
86+
// Values retrieved from getter are considered validated and constructor-impl is not executed.
87+
val boxed = boxMethod.invoke(null, value)
88+
89+
provider.findValueSerializer(outerClazz).serialize(boxed, gen, provider)
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.fasterxml.jackson.module.kotlin.test.github
2+
3+
import com.fasterxml.jackson.core.JsonGenerator
4+
import com.fasterxml.jackson.databind.SerializerProvider
5+
import com.fasterxml.jackson.databind.annotation.JsonSerialize
6+
import com.fasterxml.jackson.databind.module.SimpleModule
7+
import com.fasterxml.jackson.databind.ser.std.StdSerializer
8+
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
9+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
10+
import org.junit.Test
11+
import kotlin.test.assertEquals
12+
import kotlin.test.assertNotEquals
13+
14+
// Most of the current behavior has been tested on GitHub464, so only serializer-related behavior is tested here.
15+
class GitHub524 {
16+
@JvmInline
17+
value class HasSerializer(val value: Int?)
18+
class Serializer : StdSerializer<HasSerializer>(HasSerializer::class.java) {
19+
override fun serialize(value: HasSerializer, gen: JsonGenerator, provider: SerializerProvider) {
20+
gen.writeString(value.toString())
21+
}
22+
}
23+
24+
@JvmInline
25+
value class NoSerializer(val value: Int?)
26+
27+
data class Poko(
28+
// ULong has a custom serializer defined in Serializers.
29+
val foo: ULong = ULong.MAX_VALUE,
30+
// If a custom serializer is set, the ValueClassUnboxSerializer will be overridden.
31+
val bar: HasSerializer = HasSerializer(1),
32+
val baz: HasSerializer = HasSerializer(null),
33+
val qux: HasSerializer? = null,
34+
// If there is no serializer, it will be unboxed as the existing.
35+
val quux: NoSerializer = NoSerializer(2)
36+
)
37+
38+
@Test
39+
fun test() {
40+
val sm = SimpleModule()
41+
.addSerializer(Serializer())
42+
val writer = jacksonMapperBuilder().addModule(sm).build().writerWithDefaultPrettyPrinter()
43+
44+
// 18446744073709551615 is ULong.MAX_VALUE.
45+
assertEquals(
46+
"""
47+
{
48+
"foo" : 18446744073709551615,
49+
"bar" : "HasSerializer(value=1)",
50+
"baz" : "HasSerializer(value=null)",
51+
"qux" : null,
52+
"quux" : 2
53+
}
54+
""".trimIndent(),
55+
writer.writeValueAsString(Poko())
56+
)
57+
}
58+
59+
class SerializeByAnnotation(@get:JsonSerialize(using = Serializer::class) val foo: HasSerializer = HasSerializer(1))
60+
61+
@Test
62+
fun failing() {
63+
val writer = jacksonObjectMapper().writerWithDefaultPrettyPrinter()
64+
65+
// JsonSerialize is not working now.
66+
assertNotEquals(
67+
"""
68+
{
69+
"foo" : "HasSerializer(value=1)"
70+
}
71+
""".trimIndent(),
72+
writer.writeValueAsString(SerializeByAnnotation())
73+
)
74+
}
75+
}

0 commit comments

Comments
 (0)