Serialization of value classes implementing an interface uses underlying type of value class for array polymorphism
When a value class object is serialized the unterlying type is used. If the value class implments an interface, array polymorphism is used to store the type. However the type is set to the underlying type instead of the value class type. I am not sure if it's possible to fix this or how as the value classes are inlined.
Example:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
sealed interface Printable {
fun prettyPrint(): String
}
@JvmInline
value class Name(val s: String) : Printable {
override fun prettyPrint(): String = "Let's $s!"
}
A Name object gets serialized to
["String","name"]
when deserializing it can not be mapped for obvious reasons.
Providing ["Name","name"] on the other hand can be deserialized. So the issue is only with serialization.
Can you attach some more detailed code? To understand the situation, I would like to read the process of instantiating values and mappers and printing serialisation results.
created a demonstration project:
https://github.com/NilsWild/kotlin-serialization-issue
It works for the data class, so it is a lack of functionality in the value class support.
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonValue
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.*
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
sealed interface Id {
@get:JsonValue
val value: UUID
}
data class PersonId(override val value: UUID) : Id
data class ThingId(override val value: UUID) : Id
data class PersonOrThing(val id: Id)
class GH230 {
@Test
fun `test serialization of value class`() {
val id = PersonId(UUID.randomUUID())
val json = jacksonObjectMapper().writeValueAsString(id)
assertEquals("[\"PersonId\",\"${id.value}\"]", json)
}
@Test
fun `test serialization of data class with value class attribute`() {
val obj = PersonOrThing(PersonId(UUID.randomUUID()))
val json = jacksonObjectMapper().writeValueAsString(obj)
assertEquals("{\"id\":[\"PersonId\",\"${obj.id.value}\"]}", json)
}
@Test
fun `test deserialization of value class by string`() {
val id = PersonId(UUID.randomUUID())
val deserialized = jacksonObjectMapper().readValue("[\"PersonId\",\"${id.value}\"]", Id::class.java)
assertEquals(id, deserialized)
}
@Test
fun `test deserialization of data class with value class attribute by string`() {
val obj = PersonOrThing(PersonId(UUID.randomUUID()))
val deserialized = jacksonObjectMapper().readValue("{\"id\":[\"PersonId\",\"${obj.id.value}\"]}", PersonOrThing::class.java)
assertEquals(obj, deserialized)
}
}
However, I have done some research and could not determine if this issue can be resolved. I would love to support this feature, but unfortunately I am very busy at the moment and need time to work on this issue.
How about something like this:
class ValueClassArrayPolymorphismSerializer(private val rawClass: Class<*>, private val cache: ReflectionCache) : JsonSerializer<Any>() {
override fun serialize(value: Any, gen: JsonGenerator, serializers: SerializerProvider) {
val unboxConverter = cache.getValueClassUnboxConverter(rawClass)
val serializer = ValueClassStaticJsonValueSerializer.createOrNull(unboxConverter) ?: unboxConverter.delegatingSerializer
gen.writeStartArray()
gen.writeString(rawClass.simpleName)
serializer.serialize(value, gen, serializers)
gen.writeEndArray()
}
}
And changing the KotlinSerializers class like that:
internal class KotlinSerializers(private val cache: ReflectionCache) : SimpleSerializers() {
val valueClassSerializers = mutableMapOf<Class<*>, JsonSerializer<*>>()
override fun findSerializer(
config: SerializationConfig?,
type: JavaType,
beanDesc: BeanDescription?
): JsonSerializer<*>? {
val rawClass = type.rawClass
return when {
UByte::class.java == rawClass -> UByteSerializer
UShort::class.java == rawClass -> UShortSerializer
UInt::class.java == rawClass -> UIntSerializer
ULong::class.java == rawClass -> ULongSerializer
// The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers.
rawClass.isUnboxableValueClass() -> {
valueClassSerializers.getOrPut(rawClass){ValueClassArrayPolymorphismSerializer(rawClass, cache)}
}
else -> null
}
}
}
https://github.com/NilsWild/jackson-module-kogera
with some improvements to not break custom serialization and deserialization