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.
Comment From: poutsma
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?
Comment From: elkhart
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
orApartment
using this XML-Schema above.
Comment From: poutsma
Are you saying that using the
WebClient
with the superclass (in the exampleRealEstate
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>
andpublic 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 theJaxb2Decoder#unmarshal
method would be similar toMarshallingHttpMessageConverter#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.
Comment From: poutsma
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.