Json-B (not JSONB!), short for Json Binding, is a modern MicroProfile Java standard to convert json documents (json messages) to a java class and vice versa.
This standard is supported by most modern java application containers. But because of a definition gap in the Jax-RS standard, it is not as easy as initially thought to get proper and helpful error messages if an invalid JSON document is encountered in the POST
request body of an endpoint. The ExceptionMapper
will only work after applying a few tricks.
Helpless ExceptionMapper
Usually when using Jax-RS endpoints, you can convert any (Runtime)Exception into a useful Response entity by writing an ExceptionMapper
. For example, if you feed an invalid json document into the Json-B parser Yasson, it will throw an JsonbException
as expected.
But this cannot be converted using a traditional ExceptionMapper
, if the exception occured while converting a json document from a POST body. The following example will not work (other than may be expected):
@Path("/endpoint")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class DefaultEndpoint {
@POST
@Path("/")
public Response postInfo(final DataDto data) {
return Response.status(Response.Status.ACCEPTED)
.header("X-Supplied-Name", data.getName())
.build();
}
}
public class DataDto {
@JsonbProperty("name")
private final String name;
@JsonbCreator
public DataDto(
@JsonbProperty("name") final String name
) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public String toString() {
return new StringJoiner(", ", DataDto.class.getSimpleName() + "[", "]")
.add("name='" + this.name + "'")
.toString();
}
}
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JsonbException implements ExceptionMapper<JsonbException> {
@Override
public Response toResponse(final JsonbException jsonbEx) {
final Map<String, Object> entity = new HashMap<>();
entity.put("message", cause.getMessage());
entity.put("type", cause.getClass().getSimpleName());
return Response.status(Response.Status.BAD_REQUEST)
.entity(entity)
.type(MediaType.APPLICATION_JSON_TYPE)
.build();
}
}
The reason why this will not work: The ExceptionMapper
is applied to the JaxRS methods. But the DataDto is being parsed before even reaching that method. It is parsed by a MessageBodyReader
which is supplied by the application server. It would work, depending on the implementation of the MessageBodyReader
. It can throw almost any exception: A RuntimeException
, an IOException
or a processed WebApplicationException
. And this is exactly what OpenLiberty does.
The result: empty response content. The caller cannot see the cause of the "bad request" he caused. It may be invalid Json or just a missing field.
The solution: Catch the correct Exception
In fact a WebApplicationException
with a status code "Bad Request" (400) thrown by the message body reader is just another BadRequestException
. This one can be catched by an ExceptionMapper
.
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JsonbExceptionMapper implements ExceptionMapper<BadRequestException> {
@Override
public Response toResponse(final BadRequestException badRequestEx) {
final Map<String, Object> entity = new HashMap<>();
final Throwable cause = badRequestEx.getCause();
if (cause == null) {
// this mimics the default behaviour.
return Response
.status(Response.Status.BAD_REQUEST)
.build();
}
// adjust to your needs.
// a switch statement will also do if you have multiple causes you want to map.
if (cause instanceof JsonbException) {
entity.put("message", cause.getMessage());
entity.put("type", cause.getClass().getSimpleName());
}
return Response.status(Response.Status.BAD_REQUEST)
.entity(entity)
.type(MediaType.APPLICATION_JSON_TYPE)
.build();
}
}
Using the code from above, the caller will receive a helpful error message why his request was invalid, e.g. a missing field or an invalid character.
Example on GitHub
My example can be found on GitHub with OpenLiberty and an explanation: https://github.com/bmhm/jaxrs-jsonb-exceptionmapper