jackson xml lists deserialization recognized as duplicate keys

こ雲淡風輕ζ 提交于 2021-02-05 02:41:36

问题


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:

  1. Text are stored in null keys either as a single String or a List.
  2. 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:

  1. Plugin a serializer into XmlMapper using a guava multimap. This puts everything into lists.
  2. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!