Skip to content

Commit 7493426

Browse files
committed
Added technical documentation on value class handling
1 parent 45c7f48 commit 7493426

File tree

1 file changed

+134
-0
lines changed

1 file changed

+134
-0
lines changed

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.

0 commit comments

Comments
 (0)