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:
- There must be a field called "type" in all packets. If not, it's format error.
- If the field "type" is "sub", there must be another field called "sub_type":
- if it is "son", deserialize it as
Son
. - If it is "daughter", deserialize it as
Daughter
.
- if it is "son", deserialize it as
- 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?