Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support @XmlSeeAlso in Jaxb2XmlDecoder #30167

Closed
elkhart opened this issue Mar 22, 2023 · 4 comments
Closed

Support @XmlSeeAlso in Jaxb2XmlDecoder #30167

elkhart opened this issue Mar 22, 2023 · 4 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@elkhart
Copy link

elkhart commented Mar 22, 2023

While introducing WebClient into my current project I ran into the issue that the call

response.bodyToMono(RealEstate::class.java)

causes the error

org.springframework.core.codec.DecodingException: Could not unmarshal XML to class test.RealEstate; nested exception is javax.xml.bind.UnmarshalException
 - with linked exception:
[com.sun.istack.SAXParseException2; lineNumber: 2; columnNumber: 1; Unable to create an instance of test.RealEstate]

	at org.springframework.http.codec.xml.Jaxb2XmlDecoder.unmarshal(Jaxb2XmlDecoder.java:242)
	Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 

We have the following simplified class hierarchy:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "RealEstate", namespace = "http://anything.you.want", propOrder = {"address"})
@XmlSeeAlso({House.class, Apartment.class})
public abstract class RealEstate {
    protected String address;

    public String getAddress() { return address; }

    public void setAddress(String address) { this.address = address; }
}

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Apartment", propOrder = {"livingArea"}, namespace = "http://anything.you.want")
@XmlRootElement(name = "apartment", namespace = "http://anything.you.want")
public class Apartment extends RealEstate {
    private String livingArea;

    public String getLivingArea() {
        return livingArea;
    }

    public void setLivingArea(String livingArea) {
        this.livingArea = livingArea;
    }
}

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "House", propOrder = {"plotArea"}, namespace = "http://anything.you.want")
@XmlRootElement(name = "house", namespace = "http://anything.you.want")
public class House extends RealEstate {
    private String plotArea;

    public String getPlotArea() {
        return plotArea;
    }

    public void setPlotArea(String plotArea) {
        this.plotArea = plotArea;
    }
}

One possible response body to be decoded is

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<objects:house xmlns:objects="http://anything.you.want">
  <address>Somewhere around the corner</address>
  <plotArea>small</plotArea>
</objects:house>

Assumed that there is nothing really wrong with the given simplified example
the unmarshal seems to be the issue.

Unmarshaller unmarshaller = initUnmarshaller(outputClass);
XMLEventReader eventReader = StaxUtils.createXMLEventReader(events);
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
  return unmarshaller.unmarshal(eventReader);
}
else {
  JAXBElement<?> jaxbElement = unmarshaller.unmarshal(eventReader, outputClass);
  return jaxbElement.getValue();
}

Since RealEstate is not annotated as XmlRootElement the else block is executed which causes the error.
Using the other method without handing over the target class would work.

I cannot say if the Unmarshaller#unmarshal(XMLEventReader reader, Class<T> declaredType ) method is supposed to work when using XMLSeeAlso as described in the simplified example.

Looking into how the RestTemplate solved the task it boils down to the MarshallingHttpMessageConverter doing

Object result = this.unmarshaller.unmarshal(source);
if (!clazz.isInstance(result)) {
  throw new TypeMismatchException(result, clazz);
}
return result;

It seems that the Jaxb2XmlDecoder differentiates for some reason while the MarshallingHttpMessageConverter is doing the type check after unmarshalling.

Also asked at stackoverflow without getting any response.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 22, 2023
@poutsma
Copy link
Contributor

poutsma commented Mar 23, 2023

The WebClient needs to know the exact type to decode to, and as such has different behavior than the MarshallingHttpMessageConverter.

The block of XML seems to represent a House object, so I think that calling bodyToMono(House::class.java) would fix this. Does it?

@poutsma poutsma added the status: waiting-for-feedback We need additional information before we can continue label Mar 23, 2023
@elkhart
Copy link
Author

elkhart commented Mar 23, 2023

The WebClient needs to know the exact type to decode to, and as such has different behavior than the MarshallingHttpMessageConverter.

Are you saying that using the WebClient with the superclass (in the example RealEstate is not feasible?

And if so how to deal with APIs like the one described that return either a House or an Apartment?

Looking at
public class Jaxb2XmlDecoder extends AbstractDecoder<Object> and
public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConverter<Object>
ensuring for the type seems to be not the task of neither of these units and I guess if the implementation of the Jaxb2Decoder#unmarshal method would be similar to MarshallingHttpMessageConverter#readFromSource it'll just work.
Can you elaborate on your thoughts a bit for me to better understand @poutsma?

The block of XML seems to represent a House object, so I think that calling bodyToMono(House::class.java) would fix this. Does it?
No, since the considered API is designed to serve either House or Apartment using this XML-Schema above.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Mar 23, 2023
@poutsma
Copy link
Contributor

poutsma commented Mar 23, 2023

Are you saying that using the WebClient with the superclass (in the example RealEstate is not feasible?

I did not say anything about feasibility; I said that the WebClient needs to know the specific type to unmarshal to. In this case, that's House.

Looking at public class Jaxb2XmlDecoder extends AbstractDecoder<Object> and public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConverter<Object> ensuring for the type seems to be not the task of neither of these units and I guess if the implementation of the Jaxb2Decoder#unmarshal method would be similar to MarshallingHttpMessageConverter#readFromSource it'll just work.
Can you elaborate on your thoughts a bit for me to better understand @poutsma?

Comparing between the blocking, synchronous RestTemplate and the reactive WebClient is really an apples and oranges comparison, as they have very different design considerations. In this case, the difference is that the Jaxb2XmlDecoder was designed to generate a reactive stream of unmarshalled elements, so that if you have the following XML:

<root>
      <child>foo</child>
      <child>bar</child>
  </root>

the WebClient can create a Flux<Child>, as opposed to a single Mono<Root>. However, in order to correctly tokenise the incoming XML, the decoder needs to know the qualified name to tokenize to, before any XML is received. It discovers the name through either the XmlRootElement or XmlType annotation. In your case, because RealEstate was specified as class, the tokenizer will look for elements named RealEstate in the http://anything.you.want namespace. And because the input stream contains a house element and no RealEstate element, the decoder fails.

(Note that the JDK itself has no asynchronous XML capabilities, but if you put Aalto on the classpath, we will use that to create the stream asynchronously).

The MarshallingHttpMessageConverter does not have streaming capabilities, so it does not have the same requirements.

The block of XML seems to represent a House object, so I think that calling bodyToMono(House::class.java) would fix this. Does it?

No, since the considered API is designed to serve either House or Apartment using this XML-Schema above.

What I meant was: does bodyToMono(House::class.java) produce the desired, unmarshalled House object, given that sample XML? And I am guessing that the answer to that question is: yes.

I can see that single endpoints that can produce two different kinds of XML don't work well the the behavior described above. However, in my personal experience these endpoints are relatively uncommon, and—more importantly—I don't see any way for us to support those, given the aforementioned streaming requirements.

@poutsma poutsma closed this as not planned Won't fix, can't repro, duplicate, stale Mar 23, 2023
@poutsma poutsma removed the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 23, 2023
@poutsma poutsma self-assigned this Mar 23, 2023
@poutsma poutsma added the status: declined A suggestion or change that we don't feel we should currently apply label Mar 23, 2023
@poutsma
Copy link
Contributor

poutsma commented Mar 27, 2023

Over the weekend I thought of a way to support @XmlSeeAlso, by having a set of possible qualified names to tokenize to, as opposed to a single name. Some internal, breaking changes are necessary to add this support, so I am assigning this issue for the first 6.1 milestone.

@poutsma poutsma reopened this Mar 27, 2023
@poutsma poutsma added in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement and removed status: declined A suggestion or change that we don't feel we should currently apply labels Mar 27, 2023
@poutsma poutsma added this to the 6.1.0-M1 milestone Mar 27, 2023
@poutsma poutsma changed the title Jaxb2XmlDecoder fails to decode when using inheritance with @XmlSeeAlso (polymorphism) Support @XmlSeeAlso in Jaxb2XmlDecoder Mar 27, 2023
poutsma added a commit to poutsma/spring-framework that referenced this issue Mar 27, 2023
This commit adds support for the @XmlSeeAlso annotation in the
Jaxb2XmlDecoder. This includes

- Finding the set of possible qualified names given a class name, rather
  than a single name.
- Splitting the XMLEvent stream when coming across one of the names in
  this set.

Closes spring-projectsgh-30167
@poutsma poutsma removed the status: feedback-provided Feedback has been provided label Apr 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

3 participants