Description
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));
}
}