Returning Beautiful Validation Error Messages In Jakarta REST With Exception Mappers

Photo of Luqman Saeed by Luqman Saeed

All non-trivial enterprise applications have some sort of constraints on the data the application processes. These constraints could range from the simplest to the most complex custom built types. The default validation API on theJakarta EEPlatform, Jakarta Bean Validation has excellent out of the box support for constraining bean fields. Then with its @Valid annotation, you can trigger automatic validation of constrained objects in certain points of an application. 

When an application is exposed as a set of REST services through Jakarta REST, you can trigger bean validation within your resources by annotating your method parameters with @Valid. This way, when there is a constraint violation, the Jakarta REST runtime will automatically send a HTTP 400 Bad Request status code to the client. Depending on the server implementation, you can have a generic HTML page returned as the body of the jakarta.ws.rs.core.Response

For example the POJO below shows two String fields annotated @NotEmpty with a custom message to show should those constraints be violated on the fields. 


public class HelloEntity implements Serializable {

    @NotEmpty(message = "Entity name must be provided.")
    private String name;

    @NotEmpty(message = "A greeting message must be provided")
    private String greeting;
    
}

And this class is consumed through the following REST resource endpoint.

@Path("/hello-world")
@Produces(MediaType.APPLICATION_JSON)
public class HelloResource {
    @Inject
    private PersistenceService persistenceService;

    @POST
    public HelloEntity save(@Valid final HelloEntity hello) {
        return persistenceService.save(hello);
    }

    }

The @Valid annotation will cause any posted HelloEntity instance to be validated automatically. Execution will only proceed if the passed instance is valid. As stated above, an invocation with invalid data will result in the server returning an HTTP status 400 to the client. For example, the following shows the returned HTTP header when a invalid request is sent to the endpoint. 



POST http://localhost:8080/jee-jumpstart/api/hello-world

HTTP/1.1 400 Bad Request
Server: Payara Server  6.2022.2 #badassfish
X-Powered-By: Servlet/6.0 JSP/3.1 (Payara Server  6.2022.2 #badassfish Java/Amazon.com Inc./17)
Content-Language: 
Content-Type: text/html
Connection: close
Content-Length: 1091
X-Frame-Options: SAMEORIGIN

This HTTP response has a very generic HTML page with the message HTTP Status 400 - Bad Request. In an enterprise application, especially one where the REST resources are exposed to external clients, such a generic message is of little value in telling the client exactly what the violation is. It would be great if the server could return much more meaningful and actionable constraint violation messages such that the client will know exactly where they got it wrong. For example, the National Bank of Belgium'sREST API design guide has an example of what a meaningful error message could look like.  

To return a much more meaningful constraint violation message to the client in Jakarta REST, we can use exception mappers. This is a construct that allows you to map any exception that derives from java.lang.Throwable to a REST Response. The ConstraintViolationExceptionMapper class below shows an implementation that maps a jakarta.validation.ConstraintViolationException to a Response. 


@Provider
public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    @Context
    UriInfo uriInfo;

    @Override
    public Response toResponse(final ConstraintViolationException exception) {

        Set<ConstraintViolation<?>> constraintViolations = exception.getConstraintViolations();

        final var jsonObject = Json.createObjectBuilder()
                .add("host",uriInfo.getAbsolutePath().getHost())
                .add("resource", uriInfo.getAbsolutePath().getPath())
                .add("title", "Validation Errors");


        final var jsonArray = Json.createArrayBuilder();

        for (final var constraint : constraintViolations) {

            String className = constraint.getLeafBean().toString().split("@")[0];
            String message = constraint.getMessage();
            String propertyPath = constraint.getPropertyPath().toString().split("\\.")[2];

            JsonObject jsonError = Json.createObjectBuilder()
                    .add("class", className)
                    .add("field", propertyPath)
                    .add("violationMessage", message)
                    .build();
            jsonArray.add(jsonError);

        }

        JsonObject errorJsonEntity = jsonObject.add("errors", jsonArray.build()).build();

        return Response.status(Response.Status.BAD_REQUEST).entity(errorJsonEntity).build();
    }
}

The class implements jakarta.ws.rs.ext.ExceptionMapper mapper interface, passing ConstraintViolationException as the target exception. The interface has a single method, toReponse that is passed an instance of the thrown exception and returns a Response object. Whithin this method, you can map the given exception however your business case demands to a Response. 

In the above example, the getConstraintViolations method on the passed exception is called to get a set of jakarta.validation.ConstraintViolation that were violated. This set is then iterated over and each element is converted to a JSON object. The implementation also adds some extra context information like the host, the resource that was invoked and a title to the payload. All of these are put together into one JSON object that is set as the entity or body of the Response object. The method then returns the Response object with the HTTP 400 Bad Request status. 

The @Provider annotation on the class tells the Jakarta REST runtime to automatically register this class as a Jakarta REST extension during provider (or extension) scanning phase. It is important to note that this works as long as you have not manually overridden the getClasses method in your jakarta.ws.rs.core.Application configuration. If you have, then you will have to manually return this class as part of the returned set of classes there. 

With the exception mapper in place, a call to the same endpoint with invalid data returns the following JSON payload as the response body with the same header as seen earlier.



{
    "host": "localhost",
    "resource": "/jee-jumpstart/api/hello-world",
    "title": "Validation Errors",
    "errors": [
        {
            "class": "fish.payara.HelloEntity",
            "field": "name",
            "violationMessage": "Entity name must be provided."
        },
        {
            "class": "fish.payara.HelloEntity",
            "field": "greeting",
            "violationMessage": "A greeting message must be provided"
        }
    ]
}

This payload is much cleaner, has a lot of context and actionable information for the resource client to know what to correct in their request. This is an example of how you can use Jakarta REST exception mappers to map meaningful exception messages to your REST resource clients. So how do you currently handle constraint violation errors in your application? Lemme know in the comment section below!

Found this useful? Try some of our guides:

Comments