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
573 stars 222 forks source link

@JsonUnwrapped does not work inside list #676

Open SimonCockx opened 2 weeks ago

SimonCockx commented 2 weeks ago

@JsonUnwrapped does not unwrap items inside a list. Below is a minimal example of what I'm trying to achieve.

I'm trying to model the following simple XSD scheme in Java.

<xs:element name="document" type="Document" />
<xs:complexType name="Document">
  <xs:sequence maxOccurs="unbounded">
    <xs:element name="a" type="xs:string" />
    <xs:element name="b" type="xs:string" />
  </xs:sequence>
</xs:complexType>

There is one type Document which contains a sequence of alternating a and b elements. E.g., this is a valid XML instance:

<document>
  <a>A1</a>
  <b>B1</b>
  <a>A2</a>
  <b>B2</b>
  <a>A3</a>
  <b>B3</b>
</document>

I tried to represent this in Java using the following classes:

@JacksonXmlRootElement(localName = "document")
public class Document {
    private List<ABPair> abPairs = new ArrayList<>();

    @JsonUnwrapped
    @JacksonXmlElementWrapper(useWrapping = false)
    public List<ABPair> getABPairs() {
        return abPairs;
    }
    public Document addABPair(ABPair pair) {
        abPairs.add(pair);
        return this;
    }
}

public static class ABPair {
    private String a;
    private String b;

    public String getA() {
        return a;
    }
    public String getB() {
        return b;
    }

    public ABPair setA(String a) {
        this.a = a;
        return this;
    }
    public ABPair setB(String b) {
        this.b = b;
        return this;
    }
}

However, the @JsonUnwrapped does not seem to work in conjunction with useWrapping = false. Unit test reproducing the problem:

@Test
public void test() throws JsonProcessingException {
    XmlMapper mapper = new XmlMapper();

    Document document =
            new Document()
                    .addABPair(new ABPair().setA("A1").setB("B1"))
                    .addABPair(new ABPair().setA("A2").setB("B2"))
                    .addABPair(new ABPair().setA("A3").setB("B3"));
    assertEquals("<document><a>A1</a><b>B1</b><a>A2</a><b>B2</b><a>A3</a><b>B3</b></document>", mapper.writeValueAsString(document));
}

The actual result is "<document><abpairs><a>A1</a><b>B1</b></abpairs><abpairs><a>A2</a><b>B2</b></abpairs><abpairs><a>A3</a><b>B3</b></abpairs></document>".

Is there a way to get this working in Jackson?

cowtowncoder commented 2 weeks ago

I am not 100% sure if this is doable or not: but I don't thing @JsonUnwrapped will work to achieve that.

On figuring out working mapping: the usual way of figuring out compatible structure between Java types and XML output is to try to serialize POJOs in question to create compatible structure.

But... yeah. This is probably not possible currently: your attempt makes sense from user perspective, but I don't think it is possible with XML module handling as is.

SimonCockx commented 2 weeks ago

On the serialization side, I did manage to get something working.

The problem is that IndexedListSerializer#unwrappingSerializer returns itself, which makes sense for JSON, but could be better for XML. I replaced this in the BeanSerializerFactory#buildIndexedListSerializer with a custom UnwrappableIndexedListSerializer which is exactly the same as IndexedListSerializer except that it returns something else as unwrappingSerializer. I then let it return my own UnwrappingIndexedListSerializer, which unwraps all of the items inside a list.

On the deserialization side however, things seem worse. If I look in the code, it seems Jackson does not support deserializing multiple fields with the same name based on order. In the simpler case where no lists are involved, it already seems to fail, e.g.,

    @Test
    public void test() throws JsonProcessingException {
        ObjectMapper mapper = new XmlMapper();

        Document deserialised = mapper.readValue("<document><a>A1</a><b>B1</b><a>A2</a><b>B2</b></document>", Document.class);
        // Both `myABPair1` and `myABPair2` are assigned to the second pair... `<a>A2</a><b>B2</b>`
    }

    public static class Document {
        private ABPair myABPair1 = null;
        private ABPair myABPair2 = null;

        @JsonUnwrapped
        public ABPair getMyABPair1() {
            return myABPair1;
        }
        public Document setMyABPair1(ABPair pair) {
            myABPair1 = pair;
            return this;
        }

        @JsonUnwrapped
        public ABPair getMyABPair2() {
            return myABPair2;
        }
        public Document setMyABPair2(ABPair pair) {
            myABPair2 = pair;
            return this;
        }
    }

I'm starting to wonder: is support for order-dependent deserialisation something I can customize (even though it might be hard -- I'm adventurous :)), or does this just go too deep into the Jackson stack? Any advice?

cowtowncoder commented 1 week ago

So, yes, I can see why logically supporting @JsonUnwrapped for Lists on serialization side could make sense for XML. Unfortunately jackson-databind is supposed to be format-agnostic (although there are some minor deviations to support non-JSON formats, mostly XML, with some capability introspection; and in some cases overrides) so it's not super easy to go about that.

But as you correctly noted, the more difficult part really is deserialization, where JSON's unique names for Objects clashes with translation of XML element sequences (where repetition is common/expected).

As to order-dependant deserialization; ordering is not counted on for "Object" structured things (in JSON, from JSON Objects, in XML, translated element sequences not recognized as Arrays (... from Object model)). Or put another way, deserializers are directly responsible for reading tokens and using them, but there is no customizability exposed by annotations.

So I am not 100% sure how things could proceed here.