Polymorphic deserialization fails when `defaultImpl` is specified and a subtype contains a subtype property that is set to null
See original GitHub issueWe have a protocol where all JSON objects contain a _type
field that indicates what the JSON represents. In some cases, protocol objects contain other protocol objects as properties. We are currently using Jackson 2.8.7 for serialization and deserialization.
If we define a defaultImpl
class as a catch-all for unknown types, deserialization fails for any protocol objects that contain other protocol objects if the reference to those objects is null
. It appears that Jackson attempts to substitute the null
with an instance of the default class. This results in a com.fasterxml.jackson.databind.JsonMappingException
exception as the default class is not a subclass of the property’s type.
The following code illustrates the problem:
package com.example.test;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.LinkedHashMap;
import java.util.Map;
public class JacksonTest {
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "_type",
visible = true,
defaultImpl = Default.class)
//visible = true)
@JsonSubTypes({
@Type(value = Inner.class, name = Inner._TYPE),
@Type(value = Outer.class, name = Outer._TYPE)
})
public static class Base {
private String type;
@JsonGetter("_type")
public String type() {
return this.type;
}
@JsonSetter("_type")
public void setType(String type) {
this.type = type;
}
protected Base(String type) {
this.type = type;
}
}
public static class Inner extends Base {
public static final String _TYPE = "inner";
public Inner() {
super(_TYPE);
}
}
public static class Outer extends Base {
public static final String _TYPE = "outer";
private Inner inner;
public Outer() {
super(_TYPE);
}
@JsonGetter("inner")
public Inner inner() {
return this.inner;
}
@JsonSetter("inner")
public void setInner(Inner inner) {
this.inner = inner;
}
}
public static class Default extends Base {
private Map<String, Object> properties = new LinkedHashMap<String, Object>();
public Default() {
super("default");
}
@JsonAnySetter
public void set(String name, Object value) {
this.properties.put(name, value);
}
@JsonAnyGetter
public Map<String, Object> properties() {
return this.properties;
}
}
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
// leave 'inner' as null
Outer originalOuter = new Outer();
try {
JsonNode tree = mapper.valueToTree(originalOuter);
Base base = mapper.treeToValue(tree, Base.class);
System.out.println(base.type());
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
Running this code produces the exception
com.fasterxml.jackson.databind.JsonMappingException: Class com.example.test.JacksonTest$Default not subtype of [simple type, class com.example.test.JacksonTest$Inner]
at [Source: N/A; line: -1, column: -1]
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:305)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:268)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
at com.fasterxml.jackson.databind.DeserializationContext.findContextualValueDeserializer(DeserializationContext.java:443)
at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:188)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:112)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:97)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1089)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:63)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:3770)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2099)
at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:2596)
at com.example.test.JacksonTest.main(JacksonTest.java:101)
Caused by: java.lang.IllegalArgumentException: Class com.example.test.JacksonTest$Default not subtype of [simple type, class com.example.test.JacksonTest$Inner]
at com.fasterxml.jackson.databind.type.TypeFactory.constructSpecializedType(TypeFactory.java:359)
at com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder.buildTypeDeserializer(StdTypeResolverBuilder.java:128)
at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findTypeDeserializer(BasicDeserializerFactory.java:1373)
at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findPropertyTypeDeserializer(BasicDeserializerFactory.java:1508)
at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.resolveMemberAndTypeAnnotations(BasicDeserializerFactory.java:1857)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.constructSettableProperty(BeanDeserializerFactory.java:728)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.addBeanProps(BeanDeserializerFactory.java:516)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:226)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:141)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:403)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:349)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
... 12 more
If you comment out the defaultImpl
portion of the annotation, the application runs as expected without an exception.
My general expectation would be that the deserializer would simply set the property to null
. However, I’m somewhat new to Jackson, so perhaps I’m missing something here.
Issue Analytics
- State:
- Created 7 years ago
- Comments:5
Top GitHub Comments
Hi @thomasturrell,
One thing that I ran across when I applied this approach to my production code is that you still need to keep the
@JsonSubtypes
annotation as part of theBase
class. Your example code prints the correct type value strings, but the deserialized object types are all instances of theDefault
class.In other words, the annotations should look something like
When I updated your sample with these annotations, Jackson correctly deserialized each object to the expected subclass.
Thanks again for your help.
Hi
I believe I had a similar problem. I’m guessing that the author might say that this is a question for Stack Overflow however I think that it relates to Issue #955. If nothing else the documentation could possibly be a little clearer.
In the meantime I think that you will need to annotate your child classes with @JsonTypeInfo and @JsonSubTypes to override the inherited annotations.
However I might have misunderstood your use case. Also I’m new to Jackson, so it might be a case of the blind leading the blind.
Is the following any use to you?