Skip to content

MapDeserializer can not merge Maps with polymorphic values #2336

Closed
@rgreig

Description

@rgreig

I have found what appears to be a defect in MapDeserializer when trying to use JSON merging, with a Map where the values are polymorphic types. So for example, consider a Map<String, FooBase> where FooBase is an abstract class (annotated with JsonTypoInfo and JsonSubTypes etc). When trying to merge another structure into that map, it fails when trying to merge an existing value - i.e. where there is an existing mapping from key "SomeKey" to a concrete subclass of FooBase, it fails with exception:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `FooBase` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

Having looked at the code I believe the issue is in MapDeserializer, specifically in method _readAndUpdateStringKeyMap here:

Object old = result.get(key);
Object value;

if (old != null) {
    value = valueDes.deserialize(p, ctxt, old);
} else if (typeDeser == null) {
    value = valueDes.deserialize(p, ctxt);
} else {
   value = valueDes.deserializeWithType(p, ctxt, typeDeser);
}

Note in the above code, if there is an existing value it calls deserialize on the valueDes instance which is just an abstract deserializer rather than leveraging the type (in typeDeser). I don't know much more than that yet so I was hoping that someone could offer some guidance about whether this is easily fixed or going to be more involved. I'd be happy to work on a pull request with some guidance from people more familiar with the codebase.

Here is a fully self-contained example that illustrates the issue:

public class JacksonBugTest {

    private ObjectMapper mapper = new ObjectMapper();

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "discriminator", visible = true)
    @JsonSubTypes({@JsonSubTypes.Type(value = SomeClassA.class, name = "FirstConcreteImpl")})
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public static abstract class SomeBaseClass {
        private String name;

        @JsonCreator
        public SomeBaseClass(@JsonProperty("name") String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    @JsonTypeName("FirstConcreteImpl")
    public static class SomeClassA extends SomeBaseClass {
        private Integer a;
        private Integer b;

        @JsonCreator
        public SomeClassA(@JsonProperty("name") String name, @JsonProperty("a") Integer a, @JsonProperty("b") Integer b) {
            super(name);
            this.a = a;
            this.b = b;
        }

        public Integer getA() {
            return a;
        }

        public void setA(Integer a) {
            this.a = a;
        }

        public Integer getB() {
            return b;
        }

        public void setB(Integer b) {
            this.b = b;
        }
    }

    public static class SomeOtherClass {

        private String someprop;

        @JsonCreator
        public SomeOtherClass(@JsonProperty("someprop") String someprop) {
            this.someprop = someprop;
        }

        @JsonMerge
        private Map<String, SomeBaseClass> data = new LinkedHashMap<>();

        public void addValue(String key, SomeBaseClass value) {
            data.put(key, value);
        }

        public Map<String, SomeBaseClass> getData() {
            return data;
        }

        public void setData(
                Map<String, SomeBaseClass> data) {
            this.data = data;
        }
    }

    @Test
    public void test1() throws Exception {
        // first let's just get some valid JSON
        SomeOtherClass test = new SomeOtherClass("house");
        test.addValue("SOMEKEY", new SomeClassA("fred", 1, null));
        String serializedValue = mapper.writeValueAsString(test);
        System.out.println("Serialized value: " + serializedValue);

        // now create a reader specifically for merging
        ObjectReader reader = mapper.readerForUpdating(test);

        SomeOtherClass toBeMerged = new SomeOtherClass("house");
        toBeMerged.addValue("SOMEKEY", new SomeClassA("jim", null, 2));
        String jsonForMerging = mapper.writeValueAsString(toBeMerged);
        System.out.println("JSON to be merged: " + jsonForMerging);
        // now try to do the merge and it blows up
        SomeOtherClass mergedResult = reader.readValue(jsonForMerging);
        System.out.println("Serialized value: " + mapper.writeValueAsString(mergedResult));
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions