Skip to content

Commit aaf04ab

Browse files
authored
Merge pull request #768 from k163377/deser-value-class
Added `value class` deserialization support.
2 parents e306b8d + 71560e6 commit aaf04ab

File tree

43 files changed

+13166
-18
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+13166
-18
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ These Kotlin classes are supported with the following fields for serialization/d
159159
* CharRange _(start, end)_
160160
* LongRange _(start, end)_
161161

162+
Deserialization for `value class` is also supported since 2.17.
163+
Please refer to [this page](./docs/value-class-support.md) for more information on using `value class`, including serialization.
164+
162165
(others are likely to work, but may not be tuned for Jackson)
163166

164167
# Sealed classes without @JsonSubTypes

docs/value-class-handling.md

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
This is a document that summarizes how `value class` is handled in `kotlin-module`.
2+
3+
# Annotation assigned to a property (parameter)
4+
In `Kotlin`, annotations on properties will be assigned to the parameters of the primary constructor.
5+
On the other hand, if the parameter contains a `value class`, this annotation will not work.
6+
See #651 for details.
7+
8+
# Serialize
9+
Serialization is performed as follows
10+
11+
1. If the value is unboxed in the getter of a property, re-box it
12+
2. Serialization is performed by the serializer specified for the class or by the default serializer of `kotlin-module`
13+
14+
## Re-boxing of value
15+
Re-boxing is handled by `KotlinAnnotationIntrospector#findSerializationConverter`.
16+
17+
The properties re-boxed here are handled as if the type of the getter was `value class`.
18+
This allows the `JsonSerializer` specified for the mapper, class and property to work.
19+
20+
### Edge case on `value class` that wraps `null`
21+
If the property is non-null and the `value class` that is the value wraps `null`,
22+
then the value is re-boxed by `KotlinAnnotationIntrospector#findNullSerializer`.
23+
This is the case for serializing `Dto` as follows.
24+
25+
```kotlin
26+
@JvmInline
27+
value class WrapsNullable(val v: String?)
28+
29+
data class Dto(val value: WrapsNullable = WrapsNullable(null))
30+
```
31+
32+
In this case, features like the `JsonSerialize` annotation will not work as expected due to the difference in processing paths.
33+
34+
## Default serializers with `kotlin-module`
35+
Default serializers for boxed values are implemented in `KotlinSerializers`.
36+
There are two types: `ValueClassUnboxSerializer` and `ValueClassSerializer.StaticJsonValue`.
37+
38+
The former gets the value by unboxing and the latter by executing the method with the `JsonValue` annotation.
39+
The serializer for the retrieved value is then obtained and serialization is performed.
40+
41+
# Deserialize
42+
Deserialization is performed as follows
43+
44+
1. Get `KFunction` from a non-synthetic constructor (if the constructor is a creator)
45+
2. If it is unboxed on a parameter, refine it to a boxed type
46+
3. `value class` is deserialized by `Jackson` default handling or by `kotlin-module` deserializer
47+
4. Instantiation is done by calling `KFunction`
48+
49+
The special `JsonDeserializer`, `WrapsNullableValueClassDeserializer`, is described in the [section on instantiation](#Instantiation).
50+
51+
## Get `KFunction` from non-synthetic constructor
52+
Constructor with `value class` parameters compiles into a `private` non-synthesized constructor and a synthesized constructor.
53+
54+
A `KFunction` is inherently interconvertible with any constructor or method in a `Java` reflection.
55+
In the case of a constructor with a `value class` parameter, it is the synthetic constructor that is interconvertible.
56+
57+
On the other hand, `Jackson` does not handle synthetic constructors.
58+
Therefore, `kotlin-module` needs to get `KFunction` from a `private` non-synthetic constructor.
59+
60+
This acquisition process is implemented as a `valueClassAwareKotlinFunction` in `ReflectionCache.kt`.
61+
62+
## Refinement to boxed type
63+
Refinement to a boxed type is handled by `KotlineNamesAnnotationIntrospector#refineDeserializationType`.
64+
Like serialization, the parameters refined here are handled as if the type of the parameter was `value class`.
65+
66+
This will cause the result of reading from the `PropertyValueBuffer` with `ValueInstantiator#createFromObjectWith` to be the boxed value.
67+
68+
## Deserialization of `value class`
69+
Deserialization of `value class` may be handled by default by `Jackson` or by `kotlin-module`.
70+
71+
### by `Jackson`
72+
If a custom `JsonDeserializer` is set or a special `JsonCreator` is defined,
73+
deserialization of the `value class` is handled by `Jackson` just like a normal class.
74+
The special `JsonCreator` is a factory function that is configured to return the `value class` in bytecode.
75+
76+
The special `JsonCreator` is handled in exactly the same way as a regular class.
77+
That is, it does not have the restrictions that the mode is fixed to `DELEGATING`
78+
or that it cannot have multiple arguments.
79+
This can be defined by setting the return value to `nullable`, for example
80+
81+
```kotlin
82+
@JvmInline
83+
value class PrimitiveMultiParamCreator(val value: Int) {
84+
companion object {
85+
@JvmStatic
86+
@JsonCreator
87+
fun creator(first: Int, second: Int): PrimitiveMultiParamCreator? =
88+
PrimitiveMultiParamCreator(first + second)
89+
}
90+
}
91+
```
92+
93+
### by `kotlin-module`
94+
Deserialization using constructors or factory functions that return unboxed value in bytecode
95+
is handled by the `WrapsNullableValueClassBoxDeserializer` that defined in `KotlinDeserializer.kt`.
96+
97+
They must always have a parameter size of 1, like `JsonCreator` with `DELEGATING` mode specified.
98+
Note that the `kotlin-module` proprietary implementation raises an `InvalidDefinitionException`
99+
if the parameter size is greater than 2.
100+
101+
## Instantiation
102+
Instantiation by calling `KFunction` obtained from a constructor or factory function is done with `KotlinValueInstantiator#createFromObjectWith`.
103+
104+
Boxed values are required as `KFunction` arguments, but since the `value class` is read as a boxed value as described above,
105+
basic processing is performed as in a normal class.
106+
However, there is special processing for the edge case described below.
107+
108+
### Edge case on `value class` that wraps nullable
109+
If the parameter type is `value class` and non-null, which wraps nullable, and the value on the JSON is null,
110+
the wrapped null is expected to be read as the value.
111+
112+
```kotlin
113+
@JvmInline
114+
value class WrapsNullable(val value: String?)
115+
116+
data class Dto(val wrapsNullable: WrapsNullable)
117+
118+
val mapper = jacksonObjectMapper()
119+
120+
// serialized: {"wrapsNullable":null}
121+
val json = mapper.writeValueAsString(Dto(WrapsNullable(null)))
122+
// expected: Dto(wrapsNullable=WrapsNullable(value=null))
123+
val deserialized = mapper.readValue<Dto>(json)
124+
```
125+
126+
In `kotlin-module`, a special `JsonDeserializer` named `WrapsNullableValueClassDeserializer` was introduced to support this.
127+
This deserializer has a `boxedNullValue` property,
128+
which is referenced in `KotlinValueInstantiator#createFromObjectWith` as appropriate.
129+
130+
I considered implementing it with the traditional `JsonDeserializer#getNullValue`,
131+
but I chose to implement it as a special property because of inconsistencies that could not be resolved
132+
if all cases were covered in detail in the prototype.
133+
Note that this property is referenced by `KotlinValueInstantiator#createFromObjectWith`,
134+
so it will not work when deserializing directly.

docs/value-class-support.md

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
`jackson-module-kotlin` supports many use cases of `value class` (`inline class`).
2+
This page summarizes the basic policy and points to note regarding the use of the `value class`.
3+
4+
For technical details on `value class` handling, please see [here](./value-class-handling.md).
5+
6+
# Note on the use of `value class`
7+
`jackson-module-kotlin` supports the `value class` for many common use cases, both serialization and deserialization.
8+
However, full compatibility with normal classes (e.g. `data class`) is not achieved.
9+
In particular, there are many edge cases for the `value class` that wraps nullable.
10+
11+
The cause of this difference is that the `value class` itself and the functions that use the `value class` are
12+
compiled into bytecodes that differ significantly from the normal classes.
13+
Due to this difference, some cases cannot be handled by basic `Jackson` parsing, which assumes `Java`.
14+
Known issues related to `value class` can be found [here](https://github.com/FasterXML/jackson-module-kotlin/issues?q=is%3Aissue+is%3Aopen+label%3A%22value+class%22).
15+
16+
In addition, one of the features of the `value class` is improved performance,
17+
but when using `Jackson` (not only `Jackson`, but also other libraries that use reflection),
18+
the performance is rather reduced.
19+
This can be confirmed from [kogera-benchmark](https://github.com/ProjectMapK/kogera-benchmark?tab=readme-ov-file#comparison-of-normal-class-and-value-class).
20+
21+
For these reasons, we recommend careful consideration when using `value class`.
22+
23+
# Basic handling of `value class`
24+
A `value class` is basically treated like a value.
25+
26+
For example, the serialization of `value class` is as follows
27+
28+
```kotlin
29+
@JvmInline
30+
value class Value(val value: Int)
31+
32+
val mapper = jacksonObjectMapper()
33+
mapper.writeValueAsString(Value(1)) // -> 1
34+
```
35+
36+
This is different from the `data class` serialization result.
37+
38+
```kotlin
39+
data class Data(val value: Int)
40+
41+
mapper.writeValueAsString(Data(1)) // -> {"value":1}
42+
```
43+
44+
The same policy applies to deserialization.
45+
46+
This policy was decided with reference to the behavior as of `jackson-module-kotlin 2.14.1` and [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/value-classes.md#serializable-value-classes).
47+
However, these are just basic policies, and the behavior can be overridden with `JsonSerializer` or `JsonDeserializer`.
48+
49+
# Notes on customization
50+
As noted above, the content associated with the `value class` is not fully compatible with the normal class.
51+
Here is a summary of the customization considerations for such contents.
52+
53+
## Annotation
54+
Annotations assigned to parameters in a primary constructor that contains `value class` as a parameter will not work.
55+
It must be assigned to a field or getter.
56+
57+
```kotlin
58+
data class Dto(
59+
@JsonProperty("vc") // does not work
60+
val p1: ValueClass,
61+
@field:JsonProperty("vc") // does work
62+
val p2: ValueClass
63+
)
64+
```
65+
66+
See #651 for details.
67+
68+
## On serialize
69+
### JsonValue
70+
The `JsonValue` annotation is supported.
71+
72+
```kotlin
73+
@JvmInline
74+
value class ValueClass(val value: UUID) {
75+
@get:JsonValue
76+
val jsonValue get() = value.toString().filter { it != '-' }
77+
}
78+
79+
// -> "e5541a61ac934eff93516eec0f42221e"
80+
mapper.writeValueAsString(ValueClass(UUID.randomUUID()))
81+
```
82+
83+
### JsonSerializer
84+
The `JsonSerializer` basically supports the following methods:
85+
registering to `ObjectMapper`, giving the `JsonSerialize` annotation.
86+
Also, although `value class` is basically serialized as a value,
87+
but it is possible to serialize `value class` like an object by using `JsonSerializer`.
88+
89+
```kotlin
90+
@JvmInline
91+
value class ValueClass(val value: UUID)
92+
93+
class Serializer : StdSerializer<ValueClass>(ValueClass::class.java) {
94+
override fun serialize(value: ValueClass, gen: JsonGenerator, provider: SerializerProvider) {
95+
val uuid = value.value
96+
val obj = mapOf(
97+
"mostSignificantBits" to uuid.mostSignificantBits,
98+
"leastSignificantBits" to uuid.leastSignificantBits
99+
)
100+
101+
gen.writeObject(obj)
102+
}
103+
}
104+
105+
data class Dto(
106+
@field:JsonSerialize(using = Serializer::class)
107+
val value: ValueClass
108+
)
109+
110+
// -> {"value":{"mostSignificantBits":-6594847211741032479,"leastSignificantBits":-5053830536872902344}}
111+
mapper.writeValueAsString(Dto(ValueClass(UUID.randomUUID())))
112+
```
113+
114+
Note that specification with the `JsonSerialize` annotation will not work
115+
if the `value class` wraps null and the property definition is non-null.
116+
117+
## On deserialize
118+
### JsonDeserializer
119+
Like `JsonSerializer`, `JsonDeserializer` is basically supported.
120+
However, it is recommended that `WrapsNullableValueClassDeserializer` be inherited and implemented as a
121+
deserializer for `value class` that wraps nullable.
122+
123+
This deserializer is intended to make the deserialization result be a wrapped null if the parameter definition
124+
is a `value class` that wraps nullable and non-null, and the value on the `JSON` is null.
125+
An example implementation is shown below.
126+
127+
```kotlin
128+
@JvmInline
129+
value class ValueClass(val value: String?)
130+
131+
class Deserializer : WrapsNullableValueClassDeserializer<ValueClass>(ValueClass::class) {
132+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ValueClass {
133+
TODO("Not yet implemented")
134+
}
135+
136+
override fun getBoxedNullValue(): ValueClass = WRAPPED_NULL
137+
138+
companion object {
139+
private val WRAPPED_NULL = ValueClass(null)
140+
}
141+
}
142+
```
143+
144+
### JsonCreator
145+
`JsonCreator` basically behaves like a `DELEGATING` mode.
146+
Note that defining a creator with multiple arguments will result in a runtime error.
147+
148+
As a workaround, a factory function defined in bytecode with a return value of `value class` can be deserialized in the same way as a normal creator.
149+
150+
```kotlin
151+
@JvmInline
152+
value class PrimitiveMultiParamCreator(val value: Int) {
153+
companion object {
154+
@JvmStatic
155+
@JsonCreator
156+
fun creator(first: Int, second: Int): PrimitiveMultiParamCreator? =
157+
PrimitiveMultiParamCreator(first + second)
158+
}
159+
}
160+
```

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@
247247
<exclude>com.fasterxml.jackson.module.kotlin.ConstructorValueCreator</exclude>
248248
<exclude>com.fasterxml.jackson.module.kotlin.MethodValueCreator</exclude>
249249
<exclude>com.fasterxml.jackson.module.kotlin.TypesKt</exclude>
250+
<exclude>com.fasterxml.jackson.module.kotlin.KotlinDeserializers</exclude>
250251
<exclude>com.fasterxml.jackson.module.kotlin.ExtensionsKt#isUnboxableValueClass(java.lang.Class)</exclude>
251252
<exclude>com.fasterxml.jackson.module.kotlin.ExtensionsKt#toBitSet(int)</exclude>
252253
<exclude>com.fasterxml.jackson.module.kotlin.ExtensionsKt#wrapWithPath(com.fasterxml.jackson.databind.JsonMappingException,java.lang.Object,java.lang.String)</exclude>

release-notes/CREDITS-2.x

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Contributors:
1818
# 2.17.0 (not yet released)
1919

2020
WrongWrong (@k163377)
21+
* #768: Added value class deserialization support.
2122
* #763: Minor refactoring to support value class in deserialization.
2223
* #760: Improved processing related to parameter parsing on Kotlin.
2324
* #759: Organize internal commons.

release-notes/VERSION-2.x

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Co-maintainers:
1818

1919
2.17.0 (not yet released)
2020

21+
#768: Added value class deserialization support.
2122
#760: Caching is now applied to the entire parameter parsing process on Kotlin.
2223
#758: Deprecated SingletonSupport and related properties to be consistent with KotlinFeature.SingletonSupport.
2324
#755: Changes in constructor invocation and argument management.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.fasterxml.jackson.module.kotlin;
2+
3+
import com.fasterxml.jackson.core.JacksonException;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
import com.fasterxml.jackson.databind.DeserializationContext;
6+
import com.fasterxml.jackson.databind.JavaType;
7+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
8+
import kotlin.jvm.JvmClassMappingKt;
9+
import kotlin.reflect.KClass;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
12+
13+
import java.io.IOException;
14+
15+
/**
16+
* An interface to be inherited by JsonDeserializer that handles value classes that may wrap nullable.
17+
*/
18+
// To ensure maximum compatibility with StdDeserializer, this class is written in Java.
19+
public abstract class WrapsNullableValueClassDeserializer<D> extends StdDeserializer<D> {
20+
protected WrapsNullableValueClassDeserializer(@NotNull KClass<?> vc) {
21+
super(JvmClassMappingKt.getJavaClass(vc));
22+
}
23+
24+
protected WrapsNullableValueClassDeserializer(@NotNull Class<?> vc) {
25+
super(vc);
26+
}
27+
28+
protected WrapsNullableValueClassDeserializer(@NotNull JavaType valueType) {
29+
super(valueType);
30+
}
31+
32+
protected WrapsNullableValueClassDeserializer(@NotNull StdDeserializer<D> src) {
33+
super(src);
34+
}
35+
36+
@Override
37+
@NotNull
38+
public final Class<D> handledType() {
39+
//noinspection unchecked
40+
return (Class<D>) super.handledType();
41+
}
42+
43+
/**
44+
* If the parameter definition is a value class that wraps a nullable and is non-null,
45+
* and the input to JSON is explicitly null, this value is used.
46+
* Note that this will only be called from the KotlinValueInstantiator,
47+
* so it will not work for top-level deserialization of value classes.
48+
*/
49+
// It is defined so that null can also be returned so that Nulls.SKIP can be applied.
50+
@Nullable
51+
public abstract D getBoxedNullValue();
52+
53+
@Override
54+
public abstract D deserialize(@NotNull JsonParser p, @NotNull DeserializationContext ctxt)
55+
throws IOException, JacksonException;
56+
}

0 commit comments

Comments
 (0)