|
| 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