FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support Nested Polymorphic Deserialization When Using Annotations Automatically

chuanwise opened this issue · 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.

Would this be same as #2957?