How to treat exception thrown by setter on JAXB automatic unmarshal from request body?
java, jax-rs, jaxb, rest, web-services
Solution
Interception Exceptions with JAXB
A `ValidationEventHandler` is used to intercept the exceptions that occur during unmarshalling. If you want to collect all the errors that occur you can use a `ValidationEventCollector`.
Java Model
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Foo {
private String bar;
public String getBar() {
return bar;
}
public void setBar(String bar) {
throw new RuntimeException("Always throw an error");
}
}
Demo
import java.io.StringReader;
import javax.xml.bind.*;
import javax.xml.bind.util.ValidationEventCollector;
public class Demo {
private static String XML = "<foo><bar>Hello World</bar></foo>";
public static void main(String[] args) throws Exception {
JAXBContext jc = JAXBContext.newInstance(Foo.class);
Unmarshaller unmarshaller = jc.createUnmarshaller();
unmarshaller.unmarshal(new StringReader(XML));
ValidationEventCollector vec = new ValidationEventCollector();
unmarshaller.setEventHandler(vec);
unmarshaller.unmarshal(new StringReader(XML));
System.out.println(vec.getEvents().length);
}
}
Output
1
Integrating with JAX-RS
`MessageBodyReader`
You could create an instance of `MessageBodyReader` where you can leverage a `ValidationEventHandler` during the unmarshal process. Below is a link giving an example of how to implement a `MessageBodyReader`.
- Validate JAXBElement in JPA/JAX-RS Web Service
`ContextResolver<Unmarshaller>`
Slightly higher level, you could implement `ContextResolver<Unmarshaller>` and have the `Unmarshaller` returned have the appropriate `ValidationEventHandler` on it. It would look something like the following:
import javax.ws.rs.core.*;
import javax.ws.rs.ext.*;
import javax.xml.bind.*;
import javax.xml.bind.helpers.DefaultValidationEventHandler;
@Provider
public class UnmarshallerResolver implements ContextResolver<Unmarshaller> {
@Context Providers providers;
@Override
public Unmarshaller getContext(Class<?> type) {
try {
ContextResolver<JAXBContext> resolver = providers.getContextResolver(JAXBContext.class, MediaType.APPLICATION_XML_TYPE);
JAXBContext jaxbContext;
if(null == resolver || null == (jaxbContext = resolver.getContext(type))) {
jaxbContext = JAXBContext.newInstance(type);
}
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
unmarshaller.setEventHandler(new DefaultValidationEventHandler());
return unmarshaller;
} catch(JAXBException e) {
throw new RuntimeException(e);
}
}
}
Problem
I send the following JSON request body to my controller: ``` {"Game": {"url": "asd"}} ``` where `Game` is my model class, annotated with `@XmlRootElement` (and some JPA annotations which are not important in this context). The controller: ``` @PUT @Path("/{name}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response createRow( @PathParam("name") String name, Game gameData) throws Exception{ Game.createRow(gameData); // + exception handling etc. } ``` Now, I understood that when `Game gameData` parameter of the controller method is created, my setters from the model class are called. The setter that requires attention is: ``` public void setUrl(String url) throws Exception{ String regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"; Pattern pattern = Pattern.compile(regex); System.out.println("URL: " + url); if ( url == null || url.length() == 0) { throw new Exception("The url of the game is mandatory!"); } else { Matcher matcher = pattern.matcher(url); if (!matcher.matches()) { throw new Exception("The url is invalid! Please check its syntax!"); } else { this.url = url; } } } ``` What happens is that, during the deserialization of the JSON string to the Game object, the `The url is invalid!` exception is thrown, but only in the console of `TomEE`. What I want to do is to send this error to the client. If I use an exception which extends `WebApplicationException` instead of the generic Exception, then I get an exception in the client, but not the one about the validity of the url. Instead, after the deserialization, `gameData.url` is NULL, and when I try to create a Game instance with the data from `gameData`, the setter will be called like `gameToBeCreated.set(NULL)`, and I will get the exception `Url is mandatory`, when actually an URL was sent from the client, but with bad syntax. It was not NULL when sent from client. So, can I somehow intercept the exceptions thrown when the automatic unmarshalling happens and forward them to the client?