To main content

JAX-RS: Hilfreiche Json-B-Fehlermeldungen

Veröffentlicht von Benjamin Marwell am

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