FasterXML / jackson-dataformat-xml

Extension for Jackson JSON processor that adds support for serializing POJOs as XML (and deserializing from XML) as an alternative to JSON
Apache License 2.0
567 stars 221 forks source link

XmlMapper does not gather list elements separated with other elements. #535

Open Fuud opened 2 years ago

Fuud commented 2 years ago

jackson-dataformat-xml 2.13.1

I encountered a problem trying parse Tomcat server.xml using XmlMapper. The problem: server.xml can have multiple Connector declarations. These declaration are children of Service tag but can be separated by other entities. Once them will be separated only the last one will be injected.

I created simple test to reproduce issue (second assertion will fail):


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.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import org.junit.Test;

import java.util.List;

import static org.junit.Assert.assertEquals;

public class XmlMapperTest {

    @Test
    public void test() throws JsonProcessingException {
        XmlMapper xmlMapper = new XmlMapper();
        xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        String goodStr = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>   \n" +
                "            <A>                                       \n" +
                "                <B value=\"1\"/>                      \n" +
                "                <B value=\"2\"/>                      \n" +
                "            </A>";
        A good = xmlMapper.readValue(goodStr, A.class);
        assertEquals(2, good.b.size());

        String badStr = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>   \n" +
                "            <A>                                       \n" +
                "                <B value=\"1\"/>                      \n" +
                "                <C/>                                  \n" +
                "                <B value=\"2\"/>                      \n" +
                "            </A>";
        A bad = xmlMapper.readValue(badStr, A.class);
        assertEquals(2, bad.b.size()); // <------------------------ here will be failure
    }

    static class A {
        @JacksonXmlElementWrapper(useWrapping = false)
        @JacksonXmlProperty(localName = "B")
        List<B> b;
    }

    static class B {
        @JacksonXmlProperty(isAttribute = true)
        int value;
    }
}
Fuud commented 2 years ago

I found a workaround: readToTree and convert to target object:


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.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import org.junit.Test;

import java.util.List;

import static org.junit.Assert.assertEquals;

public class XmlMapperTest {

    @Test
    public void test_workaround() throws JsonProcessingException {
        XmlMapper xmlMapper = new XmlMapper();
        xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        String goodStr = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>   \n" +
                "            <A>                                       \n" +
                "                <B value=\"1\"/>                      \n" +
                "                <B value=\"2\"/>                      \n" +
                "            </A>";
        A good = xmlMapper.convertValue(xmlMapper.readTree(goodStr), A.class);
        assertEquals(2, good.b.size());

        String badStr = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>   \n" +
                "            <A>                                       \n" +
                "                <B value=\"1\"/>                      \n" +
                "                <C/>                                  \n" +
                "                <B value=\"2\"/>                      \n" +
                "            </A>";
        A bad = xmlMapper.convertValue(xmlMapper.readTree(badStr), A.class);
        assertEquals(2, bad.b.size()); // <------------------------ here will be ok
    }

    static class A {
        @JacksonXmlElementWrapper(useWrapping = false)
        @JacksonXmlProperty(localName = "B")
        List<B> b;
    }

    static class B {
        @JacksonXmlProperty(isAttribute = true)
        int value;
    }
}
cowtowncoder commented 2 years ago

Yes, this is an existing limitation.

The issue can also be worked around by having a setter method that appends entries instead of replacing. So something like

List<B> b = new ArrayList<>();

public void setB(List<B> values) {
   this.b.addAll(values);
}
paul-kwitkin-veeva commented 1 year ago

OMG add All did it. So simple

kjetilv commented 5 months ago

Since this is still open: This trick fails when the XML has one item in the list.

cowtowncoder commented 5 months ago

@kjetilv should not matter, should still work. But apparently not... do you have an example?

kjetilv commented 5 months ago

Encountered it yesterday, fixed it by catching the exception and retrying with a custom single-field version of the "receiving" class. (This is in an outer parsing layer, so not a big deal.) This isn't code that I can share, but I can try to come up with an isolated example.

In the meantime – have you tried it?

cowtowncoder commented 5 months ago

I am happy to try out something but would need to be shown exactly what XML is being suggested (assuming it is variation of original case), or other changes (if target type was different -- but I don't think that should be the case). My problem is that textual description in english can very often be understood in multiple ways which is why I try not guess what interpretation is correct.

And in this case, I am bit confused by reference to "custom single-field version of the "receiving" class." which might indicate that we are talking about different XML structure.

Or even more precisely, looking at earlier XML, "one item in the List" would be:

        String goodStr = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>   \n" +
                "            <A>                                       \n" +
                "                <B value=\"1\"/>                      \n" +
                "            </A>";

which ought to work just fine. Is this case not working?

kjetilv commented 5 months ago

Hi again, sorry to just drop this here and then run away. This may be my mistake, really, since I had forgotten to add the ACCEPT_SINGLE_VALUE_AS_ARRAY feature.

I will try to reproduce the behaviour I observed later!

cowtowncoder commented 5 months ago

@kjetilv ah. In case of XML, that feature might not be needed (since the "unwrapped" notation is similar for both cases). Either way, reproduction would be useful if you have time at some point.

Thanks!