'Get Jackson XMLMapper to read root element name

How do I get Jackson's XMLMapper to read the name of the root xml element when deserializing?

I am deserializing input XML to generic Java class, LinkedHashMap and then to JSON. I want to dynamically read the root element of input XML on deserialization to LinkedHashMap.

Code

XmlMapper xmlMapper = new XmlMapper();
Map entries = xmlMapper.readValue(new File("source.xml"), LinkedHashMap.class);
ObjectMapper jsonMapper = new ObjectMapper();
String json = jsonMapper.writer().writeValueAsString(entries);
System.out.println(json);

Input XML

<?xml version="1.0" encoding="ISO-8859-1"?>
<File>
  <NumLeases>1</NumLeases>
  <NEDOCO>18738</NEDOCO>
  <NWUNIT>0004</NWUNIT>
  <FLAG>SUCCESS</FLAG>
  <MESSAGE>Test Upload</MESSAGE>
  <Lease>
     <LeaseVersion>1</LeaseVersion>
     <F1501B>
        <NEDOCO>18738</NEDOCO>
        <NWUNIT>0004</NWUNIT>
        <NTRUSTRECORDKEY>12</NTRUSTRECORDKEY>
     </F1501B>
  </Lease>
</File>

Actual Output

{"NumLeases":"1","NEDOCO":"18738","NWUNIT":"0004","FLAG":"SUCCESS","MESSAGE":"Test Upload","Lease":{"LeaseVersion":"1","F1501B":{"NEDOCO":"18738","NWUNIT":"0004","NTRUSTRECORDKEY":"12"}}}  

Expected Output (Note: There is a root element named "File" present in JSON)

{"File":{"NumLeases":"1","NEDOCO":"18738","NWUNIT":"0004","FLAG":"SUCCESS","MESSAGE":"Test Upload","Lease":{"LeaseVersion":"1","F1501B":{"NEDOCO":"18738","NWUNIT":"0004","NTRUSTRECORDKEY":"12"}}}}

There's probably some switch somewhere to set it. Any help shall be appreciated.



Solution 1:[1]

Sadly there is no flag for that. It can be done with a custom implementation of com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer. (Jackson How-To: Custom Deserializers):

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser;
import java.io.File;
import java.io.IOException;
//...
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(new SimpleModule().addDeserializer(JsonNode.class, 

    new JsonNodeDeserializer() {

        @Override
        public JsonNode deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            String rootName = ((FromXmlParser)p).getStaxReader().getLocalName();
            return ctxt.getNodeFactory()
                    .objectNode().set(rootName, super.deserialize(p, ctxt));
        }
    }));

JsonNode entries = xmlMapper.readTree(new File("source.xml"));
System.out.println(entries);

Solution 2:[2]

While this Question has an accepted answer, I found that it doesn't work on the latest Jackson version 2.13.2 and uses flawed approach anyway.

new SimpleModule().addDeserializer(JsonNode.class, 
    new JsonNodeDeserializer() {
        @Override
        public JsonNode deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            String rootName = ((FromXmlParser)p).getStaxReader().getLocalName();
            return ctxt.getNodeFactory()
                    .objectNode().set(rootName, super.deserialize(p, ctxt));
        }
    }));

The .getLocalName() call will return the name of the first child element, not the actual root of the parsed input. Also, fetching just the name of the element ignores the attributes, so you'll end up with just a duplicated tag name in your output.

What to do instead?

After trying a number of workarounds, I've found only one that works properly. You have to let Jackson do its root node removal and fool it with a dummy wrapper tag.

JsonNode jsonNode = XML_MAPPER.readTree("<tag>" + nestedXmlString + "</tag>");

This will wrap the XML with a dummy <tag> which is then immediately removed and forgotten.

Then, you can work with the output tree as usual:

toXmlGenerator.writeTree(jsonNode);

Caution

However, please be aware that if your XML input String contains the XML Header declaration (<?xml...), then wrapping it with a dummy tag will result in a parsing exception. To avoid this, you'll have to first remove the declaration string from the input:

String nestedXmlString = input;
if (nestedXmlString.startsWith("<?xml")) {
    nestedXmlString = nestedXmlString.substring(nestedXmlString.indexOf("?>") + 2);
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Leprechaun