问题
I'm trying to convert xml into json using jackson-2.5.1
and jackson-dataformat-xml-2.5.1
The xml structure is received from web server and unknown, therefore I can't have java class to represent the object, and I'm trying to convert directly into TreeNode
using ObjectMapper.readTree
.
My problem is jackson failing to parse lists. It is takes only the last item of the list.
code:
String xml = "<root><name>john</name><list><item>val1</item>val2<item>val3</item></list></root>";
XmlMapper xmlMapper = new XmlMapper();
JsonNode jsonResult = xmlMapper.readTree(xml);
The json result:
{"name":"john","list":{"item":"val3"}}
If I enable failure on duplicate keys xmlMapper.enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY)
, exception is thrown:com.fasterxml.jackson.databind.JsonMappingException: Duplicate field 'item' for ObjectNode: not allowed when FAIL_ON_READING_DUP_TREE_KEY enabled
Is there any feature which fixes this problem? Is there a way for me to write custom deserializer which in event of duplicate keys turn them into array?
回答1:
I ran into the same problem and decided to roll my own using straightforward DOM. The main problem is that XML does not really lends itself to a Map-List-Object type mapping like JSon does. However, with some assumptions, it is still possible:
- Text are stored in null keys either as a single String or a List.
- Empty elements, i.e. are modeled with an empty map.
Here's the class in the hope that it might just help someone else:
public class DeXML {
public DeXML() {}
public Map<String, Object> toMap(InputStream is) {
return toMap(new InputSource(is));
}
public Map<String, Object> toMap(String xml) {
return toMap(new InputSource(new StringReader(xml)));
}
private Map<String, Object> toMap(InputSource input) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(input);
document.getDocumentElement().normalize();
Element root = document.getDocumentElement();
return visitChildNode(root);
} catch (ParserConfigurationException | SAXException | IOException e) {
throw new RuntimeException(e);
}
}
// Check if node type is TEXT or CDATA and contains actual text (i.e. ignore
// white space).
private boolean isText(Node node) {
return ((node.getNodeType() == Element.TEXT_NODE || node.getNodeType() == Element.CDATA_SECTION_NODE)
&& node.getNodeValue() != null && !node.getNodeValue().trim().isEmpty());
}
private Map<String, Object> visitChildNode(Node node) {
Map<String, Object> map = new HashMap<>();
// Add the plain attributes to the map - fortunately, no duplicate attributes are allowed.
if (node.hasAttributes()) {
NamedNodeMap nodeMap = node.getAttributes();
for (int j = 0; j < nodeMap.getLength(); j++) {
Node attribute = nodeMap.item(j);
map.put(attribute.getNodeName(), attribute.getNodeValue());
}
}
NodeList nodeList = node.getChildNodes();
// Any text children to add to the map?
List<Object> list = new ArrayList<>();
for (int i = 0; i < node.getChildNodes().getLength(); i++) {
Node child = node.getChildNodes().item(i);
if (isText(child)) {
list.add(child.getNodeValue());
}
}
if (!list.isEmpty()) {
if (list.size() > 1) {
map.put(null, list);
} else {
map.put(null, list.get(0));
}
}
// Process the element children.
for (int i = 0; i < node.getChildNodes().getLength(); i++) {
// Ignore anything but element nodes.
Node child = nodeList.item(i);
if (child.getNodeType() != Element.ELEMENT_NODE) {
continue;
}
// Get the subtree.
Map<String, Object> childsMap = visitChildNode(child);
// Now, check if this is key already exists in the map. If it does
// and is not a List yet (if it is already a List, simply add the
// new structure to it), create a new List, add it to the map and
// put both elements in it.
if (map.containsKey(child.getNodeName())) {
Object value = map.get(child.getNodeName());
List<Object> multiple = null;
if (value instanceof List) {
multiple = (List<Object>)value;
} else {
map.put(child.getNodeName(), multiple = new ArrayList<>());
multiple.add(value);
}
multiple.add(childsMap);
} else {
map.put(child.getNodeName(), childsMap);
}
}
return map;
}
}
回答2:
Underscore-java library supports this XML. I am the maintainer of the project.
Output JSON:
{
"root": {
"name": "john",
"list": {
"item": [
"val1",
{
"#item": {
"#text": "val2"
}
},
"val3"
]
}
},
"#omit-xml-declaration": "yes"
}
回答3:
I use this approach:
- Plugin a serializer into XmlMapper using a guava multimap. This puts everything into lists.
- Write out the json using SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED. This unwrapps all lists with
size==1
.
Here is my code:
@Test
public void xmlToJson() {
String xml = "<root><name>john</name><list><item>val1</item>val2<item>val3</item></list></root>";
Map<String, Object> jsonResult = readXmlToMap(xml);
String jsonString = toString(jsonResult);
System.out.println(jsonString);
}
private Map<String, Object> readXmlToMap(String xml) {
try {
ObjectMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(new SimpleModule().addDeserializer(Object.class, new UntypedObjectDeserializer() {
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
protected Map<String, Object> mapObject(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonToken t = jp.getCurrentToken();
Multimap<String, Object> result = ArrayListMultimap.create();
if (t == JsonToken.START_OBJECT) {
t = jp.nextToken();
}
if (t == JsonToken.END_OBJECT) {
return (Map) result.asMap();
}
do {
String fieldName = jp.getCurrentName();
jp.nextToken();
result.put(fieldName, deserialize(jp, ctxt));
} while (jp.nextToken() != JsonToken.END_OBJECT);
return (Map) result.asMap();
}
}));
return (Map) xmlMapper.readValue(xml, Object.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
static public String toString(Object obj) {
try {
ObjectMapper jsonMapper = new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true)
.configure(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED, true);
StringWriter w = new StringWriter();
jsonMapper.writeValue(w, obj);
return w.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
It prints
{
"list" : {
"item" : [ "val1", "val3" ]
},
"name" : "john"
}
Altogether it is a variante of this approach, which comes out without guava multimap: https://github.com/DinoChiesa/deserialize-xml-arrays-jackson
Same approach is used here: Jackson: XML to Map with List deserialization
回答4:
You can catch that exception and do something like :
List<MyClass> myObjects = mapper.readValue(input, new TypeReference<List<MyClass>>(){});
(got it from here How to use Jackson to deserialise an array of objects)
It's a hackish approach and you will have to figure out how to resume from there.
来源:https://stackoverflow.com/questions/30183305/jackson-xml-lists-deserialization-recognized-as-duplicate-keys