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.
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.
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.
For these reasons, we recommend careful consideration when using value class.
A value class is basically treated like a value.
For example, the serialization of value class is as follows
@JvmInline
value class Value(val value: Int)
val mapper = jacksonObjectMapper()
mapper.writeValueAsString(Value(1)) // -> 1This is different from the data class serialization result.
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.
However, these are just basic policies, and the behavior can be overridden with JsonSerializer or JsonDeserializer.
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.
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.
data class Dto(
@JsonProperty("vc") // does not work
val p1: ValueClass,
@field:JsonProperty("vc") // does work
val p2: ValueClass
)See #651 for details.
The JsonValue annotation is supported.
@JvmInline
value class ValueClass(val value: UUID) {
@get:JsonValue
val jsonValue get() = value.toString().filter { it != '-' }
}
// -> "e5541a61ac934eff93516eec0f42221e"
mapper.writeValueAsString(ValueClass(UUID.randomUUID()))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.
@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.
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.
@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 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.
@JvmInline
value class PrimitiveMultiParamCreator(val value: Int) {
companion object {
@JvmStatic
@JsonCreator
fun creator(first: Int, second: Int): PrimitiveMultiParamCreator? =
PrimitiveMultiParamCreator(first + second)
}
}