Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added value class deserialization support. #768

Merged
merged 21 commits into from
Feb 18, 2024
Merged
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions docs/value-class-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
`jackson-module-kotlin` supports many use cases of `value class` (`inline class`).
This page summarizes the basic policy and points to note regarding the use of the `value class`.

For technical details on `value class` handling, please see [here](./value-class-handling.md).

# Note on the use of `value class`
`jackson-module-kotlin` supports the `value class` for many common use cases, both serialization and deserialization.
However, full compatibility with normal classes (e.g. `data class`) is not achieved.
In particular, there are many edge cases for the `value class` that wraps nullable.

The cause of this difference is that the `value class` itself and the functions that use the `value class` are
compiled into bytecodes that differ significantly from the normal classes.
Due to this difference, some cases cannot be handled by basic `Jackson` parsing, which assumes `Java`.
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).

In addition, one of the features of the `value class` is improved performance,
but when using `Jackson` (not only `Jackson`, but also other libraries that use reflection),
the performance is rather reduced.
This can be confirmed from [kogera-benchmark](https://github.com/ProjectMapK/kogera-benchmark?tab=readme-ov-file#comparison-of-normal-class-and-value-class).

For these reasons, we recommend careful consideration when using `value class`.

# Basic handling of `value class`
A `value class` is basically treated like a value.

For example, the serialization of `value class` is as follows

```kotlin
@JvmInline
value class Value(val value: Int)

val mapper = jacksonObjectMapper()
mapper.writeValueAsString(Value(1)) // -> 1
```

This is different from the `data class` serialization result.

```kotlin
data class Data(val value: Int)

mapper.writeValueAsString(Data(1)) // -> {"value":1}
```

The same policy applies to deserialization.

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).
However, these are just basic policies, and the behavior can be overridden with `JsonSerializer` or `JsonDeserializer`.

# Notes on customization
As noted above, the content associated with the `value class` is not fully compatible with the normal class.
Here is a summary of the customization considerations for such contents.

## Annotation
Annotations assigned to parameters in a primary constructor that contains `value class` as a parameter will not work.
It must be assigned to a field or getter.

```kotlin
data class Dto(
@JsonProperty("vc") // does not work
val p1: ValueClass,
@field:JsonProperty("vc") // does work
val p2: ValueClass
)
```

See #651 for details.

## On serialize
### JsonValue
The `JsonValue` annotation is supported.

```kotlin
@JvmInline
value class ValueClass(val value: UUID) {
@get:JsonValue
val jsonValue get() = value.toString().filter { it != '-' }
}

// -> "e5541a61ac934eff93516eec0f42221e"
mapper.writeValueAsString(ValueClass(UUID.randomUUID()))
```

### JsonSerializer
The `JsonSerializer` basically supports the following methods:
registering to `ObjectMapper`, giving the `JsonSerialize` annotation.
Also, although `value class` is basically serialized as a value,
but it is possible to serialize `value class` like an object by using `JsonSerializer`.

```kotlin
@JvmInline
value class ValueClass(val value: UUID)

class Serializer : StdSerializer<ValueClass>(ValueClass::class.java) {
override fun serialize(value: ValueClass, gen: JsonGenerator, provider: SerializerProvider) {
val uuid = value.value
val obj = mapOf(
"mostSignificantBits" to uuid.mostSignificantBits,
"leastSignificantBits" to uuid.leastSignificantBits
)

gen.writeObject(obj)
}
}

data class Dto(
@field:JsonSerialize(using = Serializer::class)
val value: ValueClass
)

// -> {"value":{"mostSignificantBits":-6594847211741032479,"leastSignificantBits":-5053830536872902344}}
mapper.writeValueAsString(Dto(ValueClass(UUID.randomUUID())))
```

Note that specification with the `JsonSerialize` annotation will not work
if the `value class` wraps null and the property definition is non-null.

## On deserialize
### JsonDeserializer
Like `JsonSerializer`, `JsonDeserializer` is basically supported.
However, it is recommended that `WrapsNullableValueClassDeserializer` be inherited and implemented as a
deserializer for `value class` that wraps nullable.

This deserializer is intended to make the deserialization result be a wrapped null if the parameter definition
is a `value class` that wraps nullable and non-null, and the value on the `JSON` is null.
An example implementation is shown below.

```kotlin
@JvmInline
value class ValueClass(val value: String?)

class Deserializer : WrapsNullableValueClassDeserializer<ValueClass>(ValueClass::class) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ValueClass {
TODO("Not yet implemented")
}

override fun getBoxedNullValue(): ValueClass = WRAPPED_NULL

companion object {
private val WRAPPED_NULL = ValueClass(null)
}
}
```

### JsonCreator
`JsonCreator` basically behaves like a `DELEGATING` mode.
Note that defining a creator with multiple arguments will result in a runtime error.

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.

```kotlin
@JvmInline
value class PrimitiveMultiParamCreator(val value: Int) {
companion object {
@JvmStatic
@JsonCreator
fun creator(first: Int, second: Int): PrimitiveMultiParamCreator? =
PrimitiveMultiParamCreator(first + second)
}
}
```