javajacksonunmarshallingjackson-dataformat-xml

JacksonXmlRootElement root element name ignored while deserializing


I'm trying to deserialize an XML file into a Java object but, apparently, Jackson ignores the root element name.

I tried adding the @JacksonXmlRootElement annotation, following suggestions from these questions.

How can I make Jackson validate the root element name?


I tried to reproduce this behaviour with the following minimal, self-contained example:

customer.xml

<Customer>
    <FirstName>John</FirstName>
    <LastName>Smith</LastName>
</Customer>

Employee.java

@JacksonXmlRootElement(localName = "Employee")
public class Employee {
    @JacksonXmlProperty(localName = "FirstName")
    private String firstName;
    @JacksonXmlProperty(localName = "LastName")
    private String lastName;
    @JacksonXmlProperty(localName = "Salary")
    private BigDecimal salary;

    // getters and setters omitted for brevity...
}

Main method

XmlMapper mapper = new XmlMapper(new JacksonXmlModule());
String xmlContent = Files.readString(Path.of("customer.xml"));
Employee employee = mapper.readValue(xmlContent, Employee.class);

With the above code, Jackson will happily deserialize an XML document starting with the <Customer> element, despite @JacksonXmlRootElement(localName = "Employee") being present on the Employee POJO class.

Am I missing some Jackson module and/or XML mapper configuration?

I am using jackson-dataformat-xml 2.18.1.


Solution

  • With the above code, Jackson will happily deserialize an XML document starting with the element, despite @JacksonXmlRootElement(localName = "Employee") being present on the Employee POJO class.

    There is a misunderstanding about the use of the JacksonXmlRootElement and its localname attribute: the localname attribute is used not for deserialization process but only for serialization to change the root tag name like below:

    @Data
    //changed the localname to Othername
    //this change will result to a root <OtherName> tag
    @JacksonXmlRootElement(localName = "OtherName") 
    public class Employee {
        @JacksonXmlProperty(localName = "FirstName")
        private String firstName;
        @JacksonXmlProperty(localName = "LastName")
        private String lastName;
        @JacksonXmlProperty(localName = "Salary")
        private BigDecimal salary;
    }
    
    public class Main {
    
        public static void main(String[] args) throws JsonProcessingException, JsonMappingException, XMLStreamException, IOException {
            String content = """
                         <Customer>
                             <FirstName>John</FirstName>
                             <LastName>Smith</LastName>
                         </Customer>
                         """;
            XmlMapper mapper = new XmlMapper();
            Employee employee = mapper.readValue(content, Employee.class);
    
            //it will print <OtherName><FirstName>John</FirstName><LastName>Smith</LastName><Salary/></OtherName>
            System.out.println(mapper.writeValueAsString(employee));      
    }
    

    With the above code, Jackson will happily deserialize an XML document starting with the element, despite @JacksonXmlRootElement(localName = "Employee") being present on the Employee POJO class.

    Am I missing some Jackson module and/or XML mapper configuration?

    From what I know there is no builtin configuration that can guarantee you a validation of the root tag name matching the classname of a Java object, but you can obtain the expected behaviour extending the XmlMapper class and creating a new method like below:

    public class XmlMapperWithEvaluation extends XmlMapper {
    
        public <T> T readValueWithEvaluation(XMLStreamReader r, Class<T> valueType) throws IOException, XMLStreamException {
            String simpleName = valueType.getSimpleName();
            //point the root tag
            r.next();
            String localName = r.getLocalName();
            if (!simpleName.equals(localName)) {
                throw new JsonProcessingException("Classes names are different!") {
                };
            }
    
            return super.readValue(r, valueType); 
        }  
    }
    

    Then you can use it like the example below creating an XMLStreamReader to read the xml:

    public class Main {
    
        public static void main(String[] args) throws JsonProcessingException, JsonMappingException, XMLStreamException, IOException {
            String content = """
                         <Customer>
                             <FirstName>John</FirstName>
                             <LastName>Smith</LastName>
                         </Customer>
                         """;
    
            //ok the mapper read the <Customer> tag and raise no exception
            XmlMapper mapper = new XmlMapper();
            Employee employee = mapper.readValue(content, Employee.class);
            System.out.println(mapper.writeValueAsString(employee));
            
            //create xmlstreamreader to read xml
            XMLInputFactory f = XMLInputFactory.newFactory();
            StringReader in = new StringReader(content);
            XMLStreamReader sr = f.createXMLStreamReader(in);
            
            XmlMapperWithEvaluation mapperWithEvaluation = new XmlMapperWithEvaluation();
    
            //ok the XmlMapperWithEvaluation mapper raise an exception
            //cause the root tag name <Customer> and Employee classname
            //are different
            employee = mapperWithEvaluation.readValueWithEvaluation(sr, Employee.class);
        }
    
    }
    
    public class XmlMapperWithEvaluation extends XmlMapper {
    
        public <T> T readValueWithEvaluation(XMLStreamReader r, Class<T> valueType) throws IOException, XMLStreamException {
            String simpleName = valueType.getSimpleName();
            r.next();
            String localName = r.getLocalName();
            if (!simpleName.equals(localName)) {
                throw new JsonProcessingException("Classes names are different!") {
                };
            }
            return super.readValue(r, valueType); 
        }
    }