We 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.
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?
public class JacksonTest1 {
// defaultImpl could be Void.class to map unknown objects to null
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type", visible = true, defaultImpl = Default.class)
public static class Base {
private String type;
private String value;
@JsonGetter("_type")
public String type() {
return type;
}
@JsonSetter("_type")
public void setType(String type) {
this.type = type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
protected Base(String type) {
this.type = type;
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type", visible = true)
@JsonSubTypes({ @Type(value = Inner.class, name = Inner._TYPE) })
public static class Inner extends Base {
public static final String _TYPE = "inner";
public Inner() {
super(_TYPE);
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type", visible = true)
@JsonSubTypes({ @Type(value = Outer.class, name = Outer._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 inner;
}
@JsonSetter("inner")
public void setInner(Inner inner) {
this.inner = inner;
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type", visible = true)
@JsonSubTypes({ @Type(value = Default.class, name = Default._TYPE) })
public static class Default extends Base {
public static final String _TYPE = "default";
private final Map<String, Object> properties = new LinkedHashMap<>();
public Default() {
super("default");
}
@JsonAnySetter
public void set(String name, Object value) {
properties.put(name, value);
}
@JsonAnyGetter
public Map<String, Object> properties() {
return properties;
}
}
public static void main(String[] args) {
final ObjectMapper mapper = new ObjectMapper();
// leave 'inner' as null
final Outer originalOuter = new Outer();
try {
final JsonNode tree = mapper.valueToTree(originalOuter);
final Base base = mapper.treeToValue(tree, Base.class);
System.out.println(base.type());
// Serialize to default
final Base base2 = mapper.readValue("{\"value\":\"Hello World\"}", Base.class);
System.out.println(base2.type());
final Base base3 = mapper.readValue("{\"_type\":\"outer\",\"value\":null,\"inner\":null}", Base.class);
System.out.println(base3.type());
final Base base4 = mapper.readValue("{\"_type\":\"inner\",\"value\":null}", Base.class);
System.out.println(base4.type());
}
catch (final JsonProcessingException e) {
e.printStackTrace();
}
catch (final IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Hi @thomasturrell,
Thanks for the reply. I gave your suggestion a try, and it does seem to address the issue. I really appreciate the insight.
I didn't post this to StackOverflow as it seemed like buggy behavior and there were a few other tickets related to defaultImpl like you mentioned. I think the key piece of information I was missing is that the annotations are inherited by the child classes, which makes the default class apply to all of the subclasses and not just the base class. Your technique of overriding the annotations in each of the child classes makes sense now that I understand that.
Thanks again for the help.
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 the Base class. Your example code prints the correct type value strings, but the deserialized object types are all instances of the Default class.
In other words, the annotations should look something like
// ....
// defaultImpl could be Void.class to map unknown objects to null
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type", visible = true, defaultImpl = Default.class)
@JsonSubTypes({
@Type(value = Inner.class, name = Inner._TYPE),
@Type(value = Outer.class, name = Outer._TYPE)
})
public static class Base {
// .....
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type", visible = true)
public static class Inner extends Base {
// ....
}
// ....
When I updated your sample with these annotations, Jackson correctly deserialized each object to the expected subclass.
Thanks again for your help.
Interesting. My example worked for me (the types were as expected), I鈥檓 using Jackson Annotations 2.8 and core 2.8.7. I suspect the version of Jackson are you using explains the difference.
Issue #1565 seems to imply that the behaviour has changed between versions.
Thank that resolved my probleme !
Most helpful comment
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
@JsonSubtypesannotation as part of theBaseclass. Your example code prints the correct type value strings, but the deserialized object types are all instances of theDefaultclass.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.