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.
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
MyEntryclass and deserialize it as a list. Starting from the initial xml:You can define two classes named
MyRootandMyEntrylike below:You can note that now the
myMapis aListinstance that contains theMyEntryobjects, now you can deserialize the xml with the expected result: