Json-B (nicht JSONB!), kurz für Json Binding, ist ein moderne MicroProfile-Java-Standard zum Konvertieren eines JSON-Dokumentes in eine Java-Klasse.
Dieser Standard wird von vielen Java Application Servern unterstützt. Auf Grund einer "Lücke" im JAX-RS-Standard (Restful services) ist es aber nicht so einfach, für falsche Json-Dokumente im POST
-Request-Body eine vernünftige Fehlermeldung auszugeben. ExceptionMapper
helfen nur nach ein paar Tricks weiter.
Hilflose ExceptionMapper
Java-Exceptions wandelt man in Java Restful Services (JAX-RS) üblicherweise mittels ExceptionMapper
um. Gibt man dem Json-B-Parser Yasson ein ungültiges Json-Dokument für eine Java-Klasse, wirft er auch brav eine JsonbException
.
Allerdings lässt sich diese nicht über den genannten ExceptionMapper
umwandeln, wenn das Json-Dokument ein Post-Body ist. Folgendes Beispiel funktioniert wider Erwarten nicht:
@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();
}
}
Der Grund, warum der o.g. Code nicht funktioniert: Dadurch, dass das DataDto nicht in der JAX-RS-Methode, sondern bereits vorher vom MessageBodyReader
des ApplicationServers umgewandelt wird, zieht der ExceptionMapper
nicht. Denn welche Exception der MessageBodyReader
schmeißt, ist dem Implementierenden freigestellt. Neben der RuntimeException
oder einer IOException
darf auch eine WebApplicationException
geworfen werden. Und genau das macht etwa OpenLiberty.
Die Folge: Leerer Content beim Aufrufen. Er sieht nicht, welches Feld etwa fehlt.
Die Lösung: Die fertige Exception abfangen
Tatsächlich ist eine WebApplicationException
mit dem StatusCode "Bad Request" (400) aber auch nur eine BadRequestException
. Diese lässt sich mit dem ExceptionMapper
abfangen:
@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();
}
}
Mit dem oben genannten Code erhält der Aufrufer jetzt einen ResponseBody
mit der hilfreichenden Fehlermeldung, dass etwa ein Feld fehlt oder das Json-Dokument anderweitig ungültig ist.
Beispiel auf GitHub
Das Beispiel auf GitHub mit OpenLiberty und Erklärung findet ihr hier: https://github.com/bmhm/jaxrs-jsonb-exceptionmapper