FasterXML/jackson-modules-base

Add support for JAXB's XmlElementWrapper to improve JAXB-specification-compliance

raphw opened this issue · 12 comments

raphw commented

I am creating a new ticket after a comment in a previous thread on this current limitation. I summarize the problem here to avoid the need for constant backtracking.

Problem description

Jackson's XML annotation handler does not currently process the XmlElementWrapper of the JAXB specification. The annotation is originally meant to be used with XML, where the annotations avoids the need for explicitly creating a wrapper element. To create XML like the following:

<root>
  <elements>
    <element/>
    <element/>
  </elements>
</root>

JAXB allows a definition as:

class Root {
  @XmlElement(name = "element")
  @XmlElementWrapper(name = "elements")
  List<Element> elements;
}

This definition is equivalent to having elements defined as a separate object:

class Root {
  @XmlElement(name = "elements")
  Elements elements;
}
class Elements {
  @XmlElement(name = "element")
  List<Element> elements;
}

According to the JAXB-specification, both objects represent the same XML structure and will marshal/unmarshal to/from the above XML structure.

This is not the case for Jackson where the first representation will marshal to the following JSON, even if the JAXB annotation introspector is used:

{"element": [{}, {}]}

whereas the latter representation creates the following JSON:

{"elements":{"element": [{}, {}]}}

Of course, the wrapper element is redundant in JSON, but it has the unfortunate bi-effect that the very same JAXB object representation cannot be marshalled/unmarshalled into one another. If an API contract is using XSD but represents the data as JSON, it is not possible to communicate if not both client and server choose the same XSD-object representation.

Practically, this can affect handrolled JAXB object representations, but it also can cause problems if either client or server use the XEW plugin for XJC whereas the other does not. While APIs still work when using a JAXB-compliant marshaller, using Jackson will violate the contract. In theory, XJC could also add native support for XmlElementWrapper in the future what would likely increase this problem.

Suggested solution

The Jackson unmarshaller should tolerate both representations (wrapped and unwrapped) to increase compatibility, if an XmlElementWrapper tag is discovered. The marshaller should allow for configuration on whether the wrapper tag is needed.

I tried an implementation to showcase that Jackson could introduce such handling. At the same time, I am not an expert on Jackson to understand if this implementation is effective or efficient. Feedback would be appreciated! I am also happy to invest time for helping to integrate this into Jackson's JAXB support.

I also wanted to point out that this is not a similar problem to a possible @JsonWrapped annotation as it was suggested. The XmlElementWrapper wraps exactly one element that is always a list. It's implementation is therefore much simpler that I would consider support for a JsonWrapped annotation.

raphw commented

Hi @cowtowncoder,
sorry to bother you even more. My current client has a need in this, so I am in the privileged position to put paid time into a PR. This way, I could run a local fork of Jackson until that would get published. My client is on-board with that approach since it would reduce the maintenance burden.

Would you be interested in an integration?

Cheers.

@raphw I am always interested in improvements. My only concern is wrt scope of changes needed -- so while I wish I could promise to integrate it all I can say is that I will definitely consider it.

And if nothing else it may well be possible to create a module that would implement this. This could mean adding some extensibility into jackson-databind if a generally useful extension point could be defined.

ps. And no problem at all about "bothering" me :)
I owe you for all the contributions and am happy to help where I can.

raphw commented

I would certainly keep things backwards compatible but allow for configuration via the module.

I will prepare a pull request based on my suggestions, that makes it easier to discuss the code. We can see about scope and details from there.

raphw commented

I have an immediate struggle already: How would I handle that I need a dependency on the XML serializer module to support XML serialization. Would introducing such an (optional) dependency be an issue?

@raphw Right, ideally databind deals with all serialization/deserialization aspects, possibly by checking some capabilities of the backend (there are some introspection methods in JsonParser and JsonGenerator).
XML module does, however, have a small number of special overrides where absolutely required.

So... ideally as few overrides of shared functionality as possible (and jackson-databind specifically has no format-specific checks, cannot depend on XML module). But sometimes sub-classing may be needed.

Not sure if above helps, but for what it is worth XML module is by far the most customized backend for Jackson. With a few gnarly hacks. One more won't make a humongous difference if necessary.

raphw commented

The problem I am facing is that I can only communicate an additional property by registering an XmlInfo object which requires a dependency onto the XML module: https://github.com/raphw/jacksom-xml-see-also/blob/main/src/main/java/org/example/hierarchy/XmlSeeAlsoSerializerWithXmlSupport.java#L68

How would I be able to add an additional property to a serializer other then by adjusting the properties property?

Hmmh. Yeah, complexity on that class is already bit scary. I don't really have a good answer.

But looking at the original problem statement, I wonder if part of the problem is one key difference between Jackson XML module's defaults and JAXB: Jackson defaults to using "wrapper" element for Lists, whereas JAXB defaults to "unwrapped" handling.
Jackson can be configured to default to unwrapped as well (I forget what the setting is but I can dig it up if need be).
Not sure if that'd help but thought worth mentioning.

raphw commented

If you have an example on how to switch it, any pointer is appreciated! I only found this thread stating that it's not currently possible: FasterXML/jackson-databind#512

@raphw Sorry, different feature: XML List wrapping is not related to more general functionality (... which does not exist).

Disabling wrapping on individual List properties is done with annotation

 @JacksonXmlElementWrapper(useWrapping=false)
  public List<Field> fields;

but default can be changed with:

    protected XmlMapper _xmlMapper = mapperBuilder()
            .propertyNamingStrategy(new PropertyNamingStrategies.UpperCamelCaseStrategy())
            .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
            .defaultUseWrapper(false)

    // Or in 2.x directly
    xmlMapper.setDefaultUseWrapper(false);

with this setting, no wrapping is added, same as default JAXB. Default differs for historical reasons.

raphw commented

This is different to the JAXB case, though. The problem is that the class is represented on the server side as:

class Root {
  @XmlElement(name = "elements")
  Elements elements;
}
class Elements {
  @XmlElement(name = "element")
  List<Element> elements;
}

and on the client side as:

class Root {
  @XmlElement(name = "element")
  @XmlElementWrapper(name = "elements")
  List<Element> elements;
}

I would need to be able to represent both the same way. My idea was that I would expand the latter example. I would not know how to do this still given your example. Are you addressing another problem?

I only mentioned this since it is directly related to additional/missing wrapping. Not that I thought it would be the same thing necessarily.

But looking at examples I do think it is relevant: with JAXB implementation, @XmlElementWrapper is not applied by default; but Jackson behaves in a way that with JAXB would require wrapper.
That is, List values by default use wrapping with Jackson: in JAXB they don't. But Jackson configuration may be changed to default to "unwrapped" case as well; it is not the default for backwards-compatibility reasons.

raphw commented

Indeed, but the issue is that the two representations in the ticket's description would be both representing the same, valid JAXB structure. I do not think it is currently possible to marshal them to the same XML/JSON representation. Or do I miss something?