jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Support Nested Polymorphic Deserialization When Using Annotations Automatically

Open chuanwise opened this issue 1 year ago • 1 comments

Is your feature request related to a problem? Please describe.

I need to deserialize incoming packets encoded in JSON. For examples, there are some rules describe an protocol:

  1. There must be a field called "type" in all packets. If not, it's format error.
  2. If the field "type" is "sub", there must be another field called "sub_type":
    1. if it is "son", deserialize it as Son.
    2. If it is "daughter", deserialize it as Daughter.
  3. If the field "type" is not "sub", it's format error. Notice that in the most actual protocols, there are many other remaining situations.

I wrote some classes, and annotated them according to JacksonDocs - JacksonPolymorphicDeserialization:

import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Test

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    property = "type",
    include = JsonTypeInfo.As.PROPERTY,
    visible = true
)
@JsonSubTypes(
    JsonSubTypes.Type(Sub::class, name = "sub"),
)
@JsonNaming(SnakeCaseStrategy::class)
interface Base {
}

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    property = "sub_type",
    include = JsonTypeInfo.As.PROPERTY,
    visible = true
)
@JsonSubTypes(
    JsonSubTypes.Type(Son::class, name = "son"),
    JsonSubTypes.Type(Daughter::class, name = "daughter")
)
interface Sub : Base {
}

data class Son(
    val sonField: String
) : Sub

data class Daughter(
    val daughterField: String
) : Sub

class EventDataTest {
    private val objectMapper = jacksonObjectMapper()

    @Test
    fun testDeserializeBase() {
        val json = """
            {
              "type": "sub",
              "sub_type": "son",
              "son_field": "son"
            }
        """.trimIndent()
        val base = objectMapper.readValue<Base>(json)
    }
}

But it will failed:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `cn.chuanwise.onebot.lib.v11.data.event.Sub` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 2, column: 11]
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1887)
	at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:414)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1375)
	at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:274)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:170)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:136)
	at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263)
	at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4905)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3848)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3831)
	at cn.chuanwise.onebot.lib.v11.data.event.EventDataTest.testDeserializeBase(EventDataTest.kt:93)
...

Which means nested polymorphic deserialization by annotations is not supported. Sometime it's very useful, such as parsing packets.

Describe the solution you'd like

Regard JsonSubTypes.Type as the next deserialization target and check if it's polymorphic in nested, instead of trying construct its instances directly because it maybe not the final POJO class.

Usage example

Deserialize some complex protocols.

Additional context

jacksonVersion = 2.17.1

In fact, I tried to wrote custom deserializer and use @JsonDeserialize(using = ...) to annotated it. But StackOverflowError will be thrown. Just like:

@JsonDeserialize(using = BaseDeserializer::class)
interface Base

private object BaseDeserializer: StdDeserializer<Base>(Base::class.java) {
    private fun readResolve(): Any = BaseDeserializer
    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Base {
        val node = p.readValueAsTree<ObjectNode>()
        val type = node.getNotNull("type").asText()
        val mapper = p.codec as ObjectMapper
        return when (type) {
            // if it's "sub", using deserializer of `Sub`
            "sub" -> mapper.convertValue(node, Sub::class.java)
            else -> throw IllegalArgumentException("Unknown type: $type")
        }
    }
}

// ...

private object SubDeserializer: StdDeserializer<Sub>(Sub::class.java) {
    private fun readResolve(): Any = SubDeserializer
    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Sub {
        val node = p.readValueAsTree<ObjectNode>()
        val type = node.getNotNull("sub_type").asText()
        val mapper = p.codec as ObjectMapper
        return when (type) {
            "son" -> mapper.convertValue(node, Son::class.java)
            "daughter" -> mapper.convertValue(node, Daughter::class.java)
            else -> throw IllegalArgumentException("Unknown type: $type")
        }
    }
}

BaseDeserializer works, but "son" -> mapper.convertValue(node, Son::class.java) in SubDeserializer will make jackson use SubDeserializer to deserialize it instead of using default deserializer of Son, so stack overflow.

chuanwise avatar May 22 '24 04:05 chuanwise

Would this be same as #2957?

cowtowncoder avatar May 22 '24 22:05 cowtowncoder

Closing as duplicate of #2957.

cowtowncoder avatar Jul 25 '24 02:07 cowtowncoder

Temporary solution, add the following code above the annotation 【 16-Dec-2010 】 in class com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase // support nested TypeDeserializer if ((_baseType != null && _baseType.getTypeName().contains("com.qcx.camunda.modules.converter.domain")) && !_baseType.getTypeName().equals(type.getTypeName())) { return ctxt.findRootValueDeserializer(type); } /* 16-Dec-2010, tatu: Since nominal type we get here has no (generic) type parameters,

qichangleixin avatar Aug 23 '24 01:08 qichangleixin