question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Polymorphic deserialization fails when `defaultImpl` is specified and a subtype contains a subtype property that is set to null

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Comments:5

github_iconTop GitHub Comments

4reactions
trevora-edgecommented, Mar 20, 2017

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.

1reaction
thomasturrellcommented, Mar 20, 2017

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();
        }
    }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Can Jackson polymorphic deserialization be used to serialize ...
If there isn't a subtype uniquely identifiable by the subtype-specific properties, the type specified by defaultImpl value will be used.
Read more >
JsonTypeInfo (Jackson-annotations 2.6.0 API) - FasterXML
Annotation used for configuring details of if and how type information is used with JSON serialization and deserialization, to preserve information about ...
Read more >
Annotation Type JsonTypeInfo - Adobe Developer
Annotation used for configuring details of if and how type information is used with JSON serialization and deserialization, to preserve information about ...
Read more >
Security update for jackson-databind, jackson ... - SUSE
As.EXTERNAL_PROPERTY' + Polymorphic subtype deduction ignores ... READ_ONLY' fails with collections when a property name is specified + ...
Read more >
Uses of Class com.fasterxml.jackson.databind.JavaType
Contains implementation classes of deserialization part of data binding. ... Try to locate a subtype for given abstract type, to either resolve to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found