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

Serialization of value classes implementing an interface uses underlying type of value class for array polymorphism #230

Open
NilsWild opened this issue May 7, 2024 · 5 comments

Comments

@NilsWild
Copy link

NilsWild commented May 7, 2024

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.

@NilsWild NilsWild changed the title Serialization of value classes implementing an interface uses wrong type for array polymorphism Serialization of value classes implementing an interface uses underlying type of value class for array polymorphism May 7, 2024
@k163377
Copy link
Contributor

k163377 commented May 7, 2024

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.

@NilsWild
Copy link
Author

NilsWild commented May 7, 2024

created a demonstration project:

https://github.com/NilsWild/kotlin-serialization-issue

@k163377
Copy link
Contributor

k163377 commented May 11, 2024

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.

@NilsWild
Copy link
Author

NilsWild commented May 14, 2024

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

@NilsWild
Copy link
Author

https://github.com/NilsWild/jackson-module-kogera

with some improvements to not break custom serialization and deserialization

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants