Cannot deserialize (convert) unwrapped list when it's second in class using Jackson XmlWrapper

rattaman :

I am trying to use XmlMapper from Jackson to deserialize some simple xml files containing unwrapped lists.

My code:

package zm.study.xmlserialize.jackson;

import java.util.List;

import org.junit.Test;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper    ;

public class JacksonListTest {

    public static class A {
        public String c;
        @JacksonXmlElementWrapper(useWrapping=false)
        public List<String> as;
    }

    @Test
    public void deserializeTest() throws Exception
    {
        XmlMapper mapper = new XmlMapper();
        String xml = "<A><c>c</c><as>a1</as><as>a2</as></A>";
        //mapper.readValue(xml, A.class);
        mapper.convertValue(mapper.readTree(xml), A.class);
    }

}

Unfortunately the library raises an exception when doing so when the list is not first in class/xml.

The exception goes away when I remove the "c" element from xml and the class. The exception also goes away if I use readValue instead of convertValue but I need the convertMethod to work.

The exception is:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayList` out of VALUE_STRING token
 at [Source: (StringReader); line: 1, column: 18] (through reference chain: zm.study.xmlserialize.jackson.JacksonListTest$A["as"])
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
    at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1343)

...

Michał Ziober :

I am not sure it is possible to do that in this way. readTree method returns object which extends JsonNode, in that case it will be ObjectNode. ObjectNode does not accept two properties with the same name, and finally after deserialisation it represents:

{"c":"c","as":"a2"}

After that you want to convert this node to A POJO class. Default deserialiser for List expects START_ARRAY token not String. You can make it work by implementing custom converter which extends StdConverter<String, List> but list will be trimmed to one element. In that case, I think, you have to use readValue method because you need to instruct Jackson that as elements are unwrapped array.

EDIT
After your comment I realised we can trick XmlMapper to use whatever we want. It is pretty obvious that Jackson uses JsonNodeDeserializer to deserialise JsonNode-s. So, all we need to do is to find a place where we can inject our code. Fortunately there is a method _handleDuplicateField which handles our case. By default it throws exception if FAIL_ON_READING_DUP_TREE_KEY flag is enabled:

protected void _handleDuplicateField(JsonParser p, DeserializationContext ctxt,
        JsonNodeFactory nodeFactory,
        String fieldName, ObjectNode objectNode,
        JsonNode oldValue, JsonNode newValue)
    throws JsonProcessingException
{
    // [databind#237]: Report an error if asked to do so:
    if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY)) {
        ctxt.reportInputMismatch(JsonNode.class,
                "Duplicate field '%s' for ObjectNode: not allowed when FAIL_ON_READING_DUP_TREE_KEY enabled",
                fieldName);
    }
}

So, let's use this fact and extend this class:

class MergeDuplicateFieldsJsonNodeDeserializer extends JsonNodeDeserializer {
    @Override
    protected void _handleDuplicateField(JsonParser p, DeserializationContext ctxt, 
                                         JsonNodeFactory nodeFactory, String fieldName, ObjectNode objectNode, 
                                         JsonNode oldValue, JsonNode newValue) throws JsonProcessingException {
        super._handleDuplicateField(p, ctxt, nodeFactory, fieldName, objectNode, oldValue, newValue);

        ArrayNode array;
        if (oldValue instanceof ArrayNode) {
            // Merge 3-rd, 4-th, ..., n-th element to already existed array
            array = (ArrayNode) oldValue;
            array.add(newValue);
        } else {
            // Merge first two elements
            array = nodeFactory.arrayNode();
            array.add(oldValue);
            array.add(newValue);
        }
        objectNode.set(fieldName, array);
    }
}

Now, we need to register this deserialiser. Whole test could look like below:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;

import java.util.Arrays;
import java.util.List;

public class XmlMapperApp {

    public static void main(String[] args) throws Exception {
        A a = new A();
        a.c = "String";
        a.as = Arrays.asList("1", "2", "tom", "Nick");

        SimpleModule mergeDuplicatesModule = new SimpleModule("Merge duplicated fields in array");
        mergeDuplicatesModule.addDeserializer(JsonNode.class, new MergeDuplicateFieldsJsonNodeDeserializer());

        XmlMapper mapper = new XmlMapper();
        mapper.registerModule(mergeDuplicatesModule);

        String xml = mapper.writeValueAsString(a);

        System.out.println(xml);
        System.out.println(mapper.readTree(xml));
    }
}

Above code prints:

<A><c>String</c><as>1</as><as>2</as><as>tom</as><as>Nick</as></A>
{"c":"String","as":["1","2","tom","Nick"]}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=152225&siteId=1