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