When trying to deserialize XML to an object that extends an abstract base class, I’m seeing that the list of references contains the expected number of elements, but all the fields on those objects are null.
This only happens if I create an XML reader for the abstract class. If I deserialize directly to the concrete implementation all the fields have the expected value.
I’ve added the minimum working example below
Expected output (as json for readability)
{ "References": [ { "id": "1", "Type": "Secondary Image" } ] }
Actual output (as json for readability)
{ "References": [ { "id": null, "Type": null } ] }
Test Data
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Foo TypeID="ConcreteA"> <Reference ID="1" Type="Secondary Image"> <Values/> </Reference> </Foo>
Abstract base class
@JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "TypeID", visible = true) @JsonSubTypes({ @JsonSubTypes.Type(value = ConcreteClassA.class, name = "ConcreteA") }) public abstract class AbstractBase { @JacksonXmlProperty(localName = "TypeID", isAttribute = true) private String typeId; @JsonIgnore private List<Reference> references = new ArrayList<>(); @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "Reference") public List<Reference> getReferences() { return references; } @JsonSetter public AbstractBase setReferences(List<Reference> references) { this.references.addAll(references); return this; } }
Concrete Implementation
public class ConcreteClassA extends AbstractBase {}
Test Cases
public class DeserializationTest { @Test public void deserializedAbstractClass_fieldsShouldNotBeNull() throws JsonProcessingException { var mapper = new XmlMapper() .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) .deactivateDefaultTyping() .registerModule(new JacksonXmlModule()); var xmlData = readTestData(); var reader = mapper.readerFor(AbstractBase.class); var deserializedObject = reader.readValue(xmlData); assert(deserializedObject instanceof ConcreteClassA); var concreteClassA = (ConcreteClassA) deserializedObject; assert(concreteClassA.getReferences().get(0).getId() != null); } @Test public void deserializedConcreteClass_fieldsShouldNotBeNull() throws JsonProcessingException { var mapper = new XmlMapper() .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) .configure(MapperFeature.USE_BASE_TYPE_AS_DEFAULT_IMPL, true) .registerModule(new JacksonXmlModule()); var xmlData = readTestData(); var reader = mapper.readerFor(ConcreteClassA.class); var deserializedObject = reader.readValue(xmlData); assert(deserializedObject instanceof ConcreteClassA); var concreteClassA = (ConcreteClassA) deserializedObject; assert(concreteClassA.getReferences().get(0).getId() != null); } private String readTestData() { try { var datafile = getClass().getClassLoader().getResource("TestData.xml"); return Files.lines(Paths.get(datafile.toURI())).collect(Collectors.joining()); } catch (Exception e) { return ""; } } }
Advertisement
Answer
Turns out there are multiple problems, that I’ve managed to solve.
- The Jackson version I was using (2.11) had some problems with multiple elements using the same tag, not in a wrapper. This is something I was aware of, and is the reason why my setter does “addAll” instead of just setting the list
This problem was solved by upgrading to 2.12, which means that it’s no longer necessary to do this to handle unwrapped elements.
- Jackson failed to properly deserialize the items, because the setter accepts a list, which apparently breaks due to some java generic mumbo jumbo (I was never able to figure out exactly why, just that it happens).
I solved this by having the setter accept a single element, and then adding that to the list
@JsonSetter(value = "Reference") public AbstractBase setReferences(Reference reference) { this.references.add(references); return this; }