FasterXML / jackson-module-kotlin

Module that adds support for serialization/deserialization of Kotlin (http://kotlinlang.org) classes and data classes.
Apache License 2.0
1.13k stars 175 forks source link

Mutliple level of @JsonTypeInfo and @JsonSubTypes does not work #831

Closed osigida closed 1 month ago

osigida commented 1 month ago

Search before asking

Describe the bug

Multi layer hierarchy could not be deserialized and fails with an error like the following

Cannot construct instance of `com.test.B2` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

To Reproduce

package com.test
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class JacksonKotlinModuleIssue {
    private val mapper = jacksonObjectMapper()

    @Test
    fun `Hierarchy deserialization test`() {
        val c1 = C1()
        val c2 = C2()
        val c3 = C3()
        val c4 = C4()

        val c1Json = mapper.writeValueAsString(c1)
        val c2Json = mapper.writeValueAsString(c2)
        val c3Json = mapper.writeValueAsString(c3)
        val c4Json = mapper.writeValueAsString(c4)

        // works
        val c1deserialised = mapper.readValue(c1Json, C1::class.java)
        // works
        val c2deserialised = mapper.readValue(c2Json, B1::class.java)
        // works
        val c3deserialised = mapper.readValue(c3Json, B2::class.java)
        // fails
        val c4deserialised = mapper.readValue(c4Json, A::class.java)

        assertEquals(c1, c1deserialised)
        assertEquals(c2, c2deserialised)
        assertEquals(c3, c3deserialised)
        assertEquals(c4, c4deserialised)
    }
}

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.EXISTING_PROPERTY,
    property = "type",
    visible = true,
)
@JsonSubTypes(
    JsonSubTypes.Type(value = B1::class, name = "B1"),
    JsonSubTypes.Type(value = B2::class, name = "B2"),
)
sealed class A {
    abstract val type: String
}

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.EXISTING_PROPERTY,
    property = "subtype",
    visible = true,
)
@JsonSubTypes(
    JsonSubTypes.Type(value = C1::class, name = "C1"),
    JsonSubTypes.Type(value = C2::class, name = "C2"),
)
sealed class B1(
    override val type: String = "B1",
) : A() {
    abstract val subtype: String
}

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.EXISTING_PROPERTY,
    property = "subtype",
    visible = true,
)
@JsonSubTypes(
    JsonSubTypes.Type(value = C3::class, name = "C3"),
    JsonSubTypes.Type(value = C4::class, name = "C4"),
)
sealed class B2(
    override val type: String = "B2",
) : A() {
    abstract val subtype: String
}

data class C1(
    override val subtype: String = "C1",
) : B1()

data class C2(
    override val subtype: String = "C2",
) : B1()

data class C3(
    override val subtype: String = "C3",
) : B2()

data class C4(
    override val subtype: String = "C4",
) : B2()

Expected behavior

deserialisation works fine for multi-layer hierarchy

Versions

Kotlin: 1.9 Jackson-module-kotlin: 2.17.2 Jackson-databind: 2.17.2

Additional context

No response

cowtowncoder commented 1 month ago

Correct: multiple levels with differing @JsonTypeInfo is not supported by jackson-databind. Since feature itself is not provided by Kotlin module so issue would need to be in jackson-databind (there likely already is an open issue).

There are no current plans to support such feature, fwtw.

osigida commented 1 month ago

Hi @cowtowncoder! Thanks a lot for the reply! Could you please share the issue? I tried to find anything related to the problem but found very little information.

cowtowncoder commented 1 month ago

I think #2957 is what I had in mind -- not sure how close a match it is.

k163377 commented 1 month ago

I have verified that this is not a kotlin-module problem. Removing kotlin-module does not change the fact that B2 is the target.

This issue is closed as it has already been answered by @cowtowncoder .