Search code examples
javaxmljackson

Jackson: Parse XML with attribute and child tag name conjunction


I need to parse XML, containing node child, which shares the name subject for attribute of type String and unwrapped collection of type Subject.

Method setSubject(Object value) checks the type of the value given and maps it to either String or Subject field internally.

Method XmlMapper.convertValue(Object from, Class<T> to) fails on second attempt with the com.fasterxml.jackson.databind.JsonMappingException because node element is being treated as LinkedHashMap instead of ArrayList expected.

com.fasterxml.jackson.databind.JsonMappingException: Cannot deserialize value of type java.util.ArrayList[com.example.MyTest$Node] from Object value (token JsonToken.START_OBJECT) at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: com.example.MyTest$Root["child"]-]com.example.MyTest$Child["subject"])

Any ideas why is working that way?

@Test
void syntheticTest() throws JsonMappingException, JsonProcessingException {

    String xml;
    Root root;

    // #1
    xml = """
            <root>
                <child subject="first subject">
                    <subject>
                        <node param="value1" />
                        <node param="value2" />
                    </subject>
                </child>
            </root>
            """;
    root = mapper.readValue(xml, Root.class);
    assertEquals(2, root.getChild().getSubjectObj().getNodes().size());

    // #2
    xml = """
            <root>
                <child subject="second subject">
                    <subject>
                        <node param="value1" />
                    </subject>
                </child>
            </root>
            """;
    root = mapper.readValue(xml, Root.class);
    assertEquals(1, root.getChild().getSubjectObj().getNodes().size());
}

@Getter
@Setter
public static class Root {

    private Child child;

}

@Getter
public static class Child {

    private String subjectStr;

    private Subject subjectObj;

    public void setSubject(Object value) {
        if (value instanceof String str) {
            subjectStr = str;
        } else {
            subjectObj = mapper.convertValue(value, Subject.class); // <- #2 com.fasterxml.jackson.databind.JsonMappingException
        }
    }

}

@Getter
@Setter
public static class Subject {

    @JsonProperty("node")
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<Node> nodes;

}

@Getter
@Setter
public static class Node {

    private String param;

}

Solution

  • By default, Jackson deserializes a list with only one value as an object instead of an array, which causes your second example to fail. Set DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY to true:

    mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
    

    Also see Jackson deserialize single item into list.