Jackson XmlMapper XML Serialization of Java Map With Wrapper Entry Element

505 Views Asked by At

The question is about Jackson XML Map serializing and deserializing. By default Jackson expects keys of the map to be XML elements themselves.

<MyMap>
    <key1>value1</key1>
    <key2>value2</key2>
</MyMap>

But this makes it impossible to have keys which are invalid XML element names.

Is there a way in Jackson to deserialize XML with wrapping entry XML element, like below?

<MyMap>
    <MyEntry>
        <Key>key1</Key>
        <Value>value1</Value>
    </MyEntry>
    <MyEntry>
        <Key>key2</Key>
        <Value>value2</Value>
    </MyEntry>
</MyMap>

I've seen @JacksonXmlElementWrapper but it doesn't seem to allow naming an entity's XML element, it names the map's XML element instead. I don't know if it's a bug or expected behavior. But I hope there's a way around it.

The demonstration code I am using in case somebody wants to reproduce

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.*;
import lombok.*;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

public class ReproductionTest {

    private static final String XML = """
<MyRoot>
    <MyMap>
        <MyEntry><Key>key1</Key><Value>value1</Value></MyEntry>
        <MyEntry><Key>key2</Key><Value>value2</Value></MyEntry>
        <!--33Key3>Not an option since I don't control keys to have valid XML names</33Key3-->
    </MyMap>
    <MyString>abc</MyString>
</MyRoot>""";

    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    static class MyRoot {
        @JacksonXmlProperty(localName = "MyMap")
        // With the next line uncommented, the following exception is thrown:
        // UnrecognizedPropertyException: Unrecognized field "MyMap"...
        // not marked as ignorable (2 known properties: "MyString", "MyEntry"])
        //@JacksonXmlElementWrapper(localName = "MyEntry")
        Map<String, String> myMap;
        @JacksonXmlProperty(localName = "MyString")
        String myString;
    }

    @Test
    public void reproducesDeserializationIssue() throws JsonProcessingException {
        XmlMapper xmlMapper = new XmlMapper();
        MyRoot myRoot = xmlMapper.readValue(XML, MyRoot.class);
        xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); // re-enforcing default
        assertEquals("abc", myRoot.getMyString());          // passes
        assertEquals(1, myRoot.getMyMap().size());          // passes
        assertNotNull(myRoot.getMyMap().get("MyEntry"));    // passes, unexpectedly
        assertEquals("", myRoot.getMyMap().get("MyEntry")); // passes, unexpectedly
        assertNull(myRoot.getMyMap().get("key1"));          // passes, unexpectedly
        assertNull(myRoot.getMyMap().get("key2"));          // passes, unexpectedly
        System.out.println(xmlMapper.writeValueAsString(myRoot));
        // Prints <MyRoot><myMap><myItem></myItem></myMap><myString>abc</myString></MyRoot>
    }
}

P.S. At this point I don't have control over XML schema, nor output POJOs. But I have control over XmlMapper configuration. So it's my understanding that my options are limited to MixIns and custom deserializers. But hope there's an easier solution.

1

There are 1 best solutions below

1
dariosicily On

As you observed in your post the problem stands in the fact that the usual standard way of deserializing a map cannot be applied to the xml you provided, but you can interpret the xml as a list of MyEntry class and deserialize it as a list. Starting from the initial xml:

<MyMap>
    <MyEntry>
        <Key>key1</Key>
        <Value>value1</Value>
    </MyEntry>
    <MyEntry>
        <Key>key2</Key>
        <Value>value2</Value>
    </MyEntry>
</MyMap>

You can define two classes named MyRoot and MyEntry like below:

@Data
public class MyRoot {

    //a MyEntry tag is a MyEntry element of the list
    @JacksonXmlProperty(localName = "MyEntry")
    @JacksonXmlElementWrapper(useWrapping = false)
    List<MyEntry> myMap;
}

@Data
public class MyEntry {

    @JacksonXmlProperty(localName = "Key")
    private String key;
    @JacksonXmlProperty(localName = "Value")
    private String value;
}

You can note that now the myMap is a List instance that contains the MyEntry objects, now you can deserialize the xml with the expected result:

XmlMapper xmlMapper = new XmlMapper();
MyRoot myRoot = xmlMapper.readValue(xml, MyRoot.class);
//ok, it prints MyRoot(myMap=[MyEntry(key=key1, value=value1), MyEntry(key=key2, value=value2)])
System.out.println(myRoot);