Search code examples
javaspringjacksonobjectmapperjackson-dataformat-xml

How to force XmlMapper to retain the root element?


I have the following minimal code:

final var xmlString = "<parent><child>data</child></parent>";

final var xmlMapper = XmlMapper.xmlBuilder()
    .enable(SerializationFeature.INDENT_OUTPUT)
    .build();

final var tree = xmlMapper.reader().readTree(xmlString);

final var formattedXmlString = xmlMapper.writeValueAsString(tree);

System.out.println(formattedXmlString);

My goal is to parse and beautify the XML string for the sake of logging. I have available xmlString on the input.

The code yields the XML beautified, but it ignores the root node caused by parsing to JSON tree (that is the default behavior of ObjectMapper that is a parent of XmlMapper).

<ObjectNode>
  <child>data</child>
</ObjectNode>

I expect the following instead:

<parent>
  <child>data</child>
</parent>

How to change <ObjectNode> root to the original <parent>? I have experimented with wrapping and unwrapping, but no luck. I also don't have any POJO object to annotate and use as the content of xmlString can be anything XML-ish.

I want to avoid manual parsing of the root element name and Regex substitution. Although this workaround works, it feels completely wrong:

final var tree = xmlMapper.reader().readTree("<_>" + xmlString + "</_>");

final var formattedXmlString = xmlMapper
    .writeValueAsString(tree)
    .replaceFirst("^<ObjectNode>\n", "")
    .replaceAll("</ObjectNode>$", "")
    .replaceAll(" {2}(.*)", "$1");

Solution

  • Ok, here is the solution.

    Though this solution worked to retain the root element using the Stax reader, the whole thing still got written by the ToXmlGenerator wrapped in the obscure <ObjectNode> element.

    <ObjectNode>
      <parent>
        <child>data</child>
      </parent>
    </ObjectNode>
    

    One more thing that I was missing was enabling the unwrapping feature from the object node that is disabled by default:

    .enable(ToXmlGenerator.Feature.UNWRAP_ROOT_OBJECT_NODE)
    

    Here is the full code:

    final var xmlString = "<parent><child>data</child></parent>";
    
    final var xmlMapper = XmlMapper.xmlBuilder()
        .enable(SerializationFeature.INDENT_OUTPUT)
        .enable(ToXmlGenerator.Feature.UNWRAP_ROOT_OBJECT_NODE)
        .defaultUseWrapper(false)
        .addModule(new SimpleModule()
            .addDeserializer(JsonNode.class, new JsonNodeDeserializer() {
                @Override
                public JsonNode deserialize(JsonParser p, DeserializationContext context) throws IOException {
                    final var rootNode = super.deserialize(p, context);
                    final var rootName = ((FromXmlParser)p).getStaxReader().getLocalName();
                    return context.getNodeFactory().objectNode().set(rootName, rootNode);
                }
            })
        )
        .build();
    final var tree = xmlMapper.readTree(xmlString);
    final var formattedXmlString = xmlMapper.writeValueAsString(tree);
    System.out.println(formattedXmlString);
    
    <parent>
      <child>data</child>
    </parent>
    

    Edit: The significant drawback of this solution and using XmlMapper without annotated entities is that it ignores XML namespaces and attributes. In such a case, I would opt for this solution instead.