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

Each list element is wrapped into an extra tag with the name of the list property (RFE: combine type id, property name) #230

Open repolevedavaj opened 7 years ago

repolevedavaj commented 7 years ago

I am implementing a Java class for calling an external API with uses XML as data format. The API provider provides for each Endpoint the corresponding XML schema. I used the XJC compiler to create schematically compliant Java classes. Now I have the problem, that the object mapper does wrap list elements into extra tags and therefore, the API call is rejected.

XML schema:

<xs:element name="body" minOccurs="0">
  <xs:complexType>
    <xs:sequence minOccurs="1" maxOccurs="unbounded">
      <xs:element name="transaction" minOccurs="0">...</xs:element>
      <xs:element name="error" type="error" minOccurs="0"/>
      <xs:element type="xs:string" name="errorEmail" minOccurs="0">...</xs:element>
    </xs:sequence>
  </xs:complexType>
</xs:element>

Java class:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "transactionAndErrorAndErrorEmail"
})
public static class Body {
    @XmlElements({
        @XmlElement(name = "transaction", type = Transaction.class),
        @XmlElement(name = "error", type = Error.class),
        @XmlElement(name = "errorEmail", type = String.class)
    })
    protected List<Object> transactionAndErrorAndErrorEmail;
    ...
}

Object Mapper Configuration:

 final ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
                .xml()
                .modules(new JaxbAnnotationModule())
                .defaultUseWrapper(false)
                .serializationInclusion(JsonInclude.Include.NON_NULL)
                .build();

Generated XML:

<body >
  <transactionAndErrorAndErrorEmail>
    <transaction>
      ...
    </transaction>
  </transactionAndErrorAndErrorEmail>
  <transactionAndErrorAndErrorEmail>
    <error>
      ...
    </error>
  </transactionAndErrorAndErrorEmail>
</body>

What I expected:

<body >
    <transaction>
      ...
    </transaction>
    <error>
      ...
    </error>
</body>

I am working with Jackson version 2.8.8, but I tested it also with version 2.9.0.pr2, but without success.

grzesuav commented 6 years ago

I have the same issue using Jackson annotations :

public class XmlMapperTest {

    private XmlMapper mapper = create();

    private XmlMapper create() {
        XmlMapper xmlMapper = new XmlMapper();
        return xmlMapper;
    }

    @Test
    public void shouldNotWrap() throws JsonProcessingException {
        Channel test = new Channel();
        test.items.add(new Pen());
        test.items.add(new Pen());
        test.items.add(new Pencil());
        System.out.println(mapper.writeValueAsString(test));
    }

    public class Channel {
        @JacksonXmlElementWrapper(localName = "items")
        public final List<Item> items = new ArrayList<Item>();
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
    @JsonSubTypes({
            @JsonSubTypes.Type(value = Pen.class, name = "pen"),
            @JsonSubTypes.Type(value = Pencil.class, name = "pencil")}
    )
    public abstract class Item {    }
    public class Pen extends Item {    }
    public class Pencil extends Item {    }

}

produces

<Channel>
    <items>
        <items>
            <pen/>
        </items>
        <items>
            <pen/>
        </items>
        <items>
            <pencil/>
        </items>
    </items>
</Channel>

but I am expecting

<Channel>
    <items>
            <pen/>
            <pen/>
            <pencil/>
    </items>
</Channel>

It seems to be regression as I have found fixed bugs related to that : https://github.com/FasterXML/jackson-module-jaxb-annotations/issues/51 https://github.com/FasterXML/jackson-dataformat-xml/issues/178 https://github.com/FasterXML/jackson-dataformat-xml/issues/159 https://github.com/FasterXML/jackson-dataformat-xml/issues/197

If there is an error in my code - please give me an advice where.

Regards

grzesuav commented 6 years ago

I have tested on Jackson version 2.9.6 for the record

nortonwong commented 5 years ago

This is specifically occurring with polymorphic lists -- the abstract class's "localName" is written even if a more specific subtype is provided via @XmlElements or @JsonSubTypes. Still happening in 2.9.8.

nortonwong commented 5 years ago

The only workaround at the moment seems to be writing a custom serializer or deserializer for the owner of the list, e.g. Body or Channel.

vortexkd commented 4 years ago

In case anybody is here looking for a work around, I ended up using something like this:

public class CustomListSerializer extends StdSerializer<List<MyClass>> {

    public CustomListSerializer() {
        this(null);
    }

    public CustomListSerializer(Class<List<MyClass>> t) {
        super(t);
    }
    @Override
    public void serialize(
            List<MyClass> value, JsonGenerator jgen, SerializerProvider provider) throws
            IOException, JsonProcessingException {
        jgen.writeStartObject();
        for (MyClass item: value) {
            jgen.writeNullField(item.getValue());
        }
        jgen.writeEndObject();
    }
}

and annotated the attribute this way:

    @JsonSerialize(using = CustomListSerializer.class)
    @JacksonXmlElementWrapper(useWrapping = false)
    @JsonProperty("Items")
    private List<MyClass> myList = new ArrayList<>();

hopefully it helps someone.

cherlo commented 4 years ago

Had same issue. Fixed it with the annotations. Key was putting both wrapper AND the property annotation on the list. No custom serializers needed:

@JacksonXmlElementWrapper(localName = "FooList")
@JacksonXmlProperty(localName = "Foo")
List<Foo> fooList;
cowtowncoder commented 4 years ago

For non-polymorphic types, annotations mentioned above should work to either include or not property name -- this is a feature, not bug.

Further, just because xjc generates some definition does not mean Jackson necessarily can use classes: XML module's contract is quite simple:

"jackson-dataformat-xml should be able to READ what it WRITEs -- it does not necessarily map all arbitrary XML structures into Java types; or support all possible Java structures (for example, Lists of Lists are NOT supported)".

So just because Jackson produces different XML structure than original poster wanted is not in itself considered a flaw (although is incompatibility with JAXB/xjc -- Jackson is not a JAXB implementation). Criteria would be whether Jackson can successfully write specific Java value and read it back.

Second: if specific output structure is desired, this may be a valid request for enhancement. For that I would need full test class and code (code included is almost there).

cowtowncoder commented 4 years ago

One other note: Jackson does not "compress" polymorphic types, and as such values as serialized are considered working as expected. Whether improvement could be implemented to omit certain levels is uncertain.

fkrauthan commented 4 years ago

Any news on this? I am currently trying to call an API that uses a XML based query language where you have something like:

    <filter>
        <and>
            <equalto>
               <field>WHENMODIFIED</field>
               <value>05/14/2020 17:09:33</value>
             </equalto>
        </and>
    </filter>

so I have a And and EqualTo class. (and a base class FilterField). But EqualTo class has

@JacksonXmlElementWrapper(useWrapping = false)
List<FilterField> field;

but for some reason the actual output is (similar to other people here):

    <filter>
        <and>
           <filter>
               <equalto>
                  <field>WHENMODIFIED</field>
                  <value>05/14/2020 17:09:33</value>
                </equalto>
             </filter>
        </and>
    </filter>

Instead of just picking the type name as defined as part of my JsonSubTypes it wraps it in the propertyName of my And class.

cowtowncoder commented 4 years ago

@fkrauthan This is not enough to reproduce the issue you have, or know what is going on, without full class definitions: for example is FilterField polymorphic (has @JsonTypeInfo) or not. As such this may be same, related or different issue.

It would make sense to file a separate issue with your details.

goatfryed commented 3 years ago

Hey hey @cowtowncoder , I came here today following a trail of issues starting in 2018. I've created a test that describes the expectation me and seemingly other users have and the actual outcome.

Note that the test and my following comments are to explain the issue and the feature/change request, even though I understand that the configuration provided may not be the one to actually yield the expected output, going forward.

The current issue here stems from the processing and interpretation of JsonTypeInfo.As.WRAPPER_OBJECT

If we compare to the json format, this means that a list such as

[{"some": "property", "type": "typeName"}]

gets transformed to

[{"typeName": {"some": "property"}}]

and in json landspace, this makes totally sense, as there is no other way to name this object.

Now, in xml landspace, every element inside an array is a actually a named element and and it would be nice to be able to rename this element instead of introducing a new wrapper element inside of it.

The current implementation generates

<Person>
    <hobbies>
        <hobbies>
            <reading>
                <favoriteBook>moby dick</favoriteBook>
            </reading>
        </hobbies>
        <hobbies>
            <biking>
                <bikeType>mountain bike</bikeType>
            </biking>
        </hobbies>
    </hobbies>
</Person>

where the inner hobbies is the equivalent to our unnamed json object, instead of the more desired

<Person>
    <hobbies>
        <reading>
            <favoriteBook>moby dick</favoriteBook>
        </reading>
        <biking>
            <bikeType>mountain bike</bikeType>
        </biking>
    </hobbies>
</Person>

This issue currently seems to mix two topics. People trying to achive the more natural xml representation i described, try to use @JsonSubTypes that don't yield the desired output. People coming from JAXB expect the @XMLElements annotation to work as described, but it seems like it simply isn't supported and should be evaluated, only when the above concludes.

cowtowncoder commented 3 years ago

@goatfryed while I appreciate your adding more information, the problem is not I do not understand what users ideally would want to have -- it is that this conflation is not possible at this point: technically concept of property name and type id MUST be separate by Jackson handlers; there is no way around that.

I have left this issue open since at some point someone might be able to figure out how to support such combination; but at this point there is no plan.

goatfryed commented 3 years ago

I thought so already, but it wasn't obvious to me what's the state of this issue. Maybe it will help other users to understand and make a decision whether they it fits their needs.

But just to clarify a bit more: Is the difficulty you're describing more within deserialization of such structures or both parts?

cowtowncoder commented 3 years ago

@goatfryed I am not sure what you mean by "both parts"? The problem is specifically with separation of type id deserialization (with TypeDeserializer) and value deserialization (main JsonDeserializer): the two interact so that conceptually you can think of JsonDeserializer being wrapped within TypeDeserializer for polymorphic use cases. Actually it gets bit more complicated than this: for POJOs (BeanDeserializer, a JsonDeserializer implementation) maps property names to value deserializers, which are potentially wrapped by TypeDeserializer. While there are multiple inclusion styles they are designed to map to JSON-style structure; and then there is no style that would flatten property name and type id into one.

It would be easy enough to add a new type id inclusion mechanism, say JsonTypeInfo.As.FLATTENED_PROPERTY (or whatever), but that would require essentially using "type id" value as "property name" by BeanDeserializer (and TypeDeserializer using it too, but that'd probably not be a big problem).

So it is the bundling of conceptually separate (but in case of XML/JAXB, combined/bundled) identitifiers (one for property, one for subtype id) that has no handling in jackson-databind.