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)) // -> 1
This 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)
}
}