Skip to content

Jackson XML deserialization of abstract type results in fields with null values

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 ""; }
    }
}

Answer

Turns out there are multiple problems, that I’ve managed to solve.

  1. 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.

  1. 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;
}