Learn, Build, Deploy

Spring Boot Validation and Error Handling – Part 1

Table of Contents

Request Validation in Spring Boot and JPA

In any application you develop, you should always think about the possibilities of something going wrong. By doing this, you can create more resilient systems that can handle failure gracefully rather than bringing your service down entirely. Remember, we cannot trust that the consumers of the API we create are going to utilise it correctly. As a result, within our service, we should validate incoming requests, and gracefully handle invalid data through out our service for the best user experience.

In the previous series, we built a happy-path Reservation Management Service (RMS), which enabled consumers to create, read, update and delete reservations.

You can grab all the starter files for the this 2 part series here, fork the RMS project and we can get started! The RMS makes use of the following dependencies: spring-boot-starter-data-jpa, spring-boot-starter-web, lombok and h2 in-memory database.

Overview of the RMS Project

The Reservation Management System (RMS) is a RESTful CRUD API in which users can create, read, update and delete reservations. When a customer wants to create a new reservation, they must provide a first and last name, as this is used in order to create a unique reservationId. Currently, if a customer wants to read, update or delete a reservation, we are assuming that the data that they pass in are valid. Please check the README for a more detailed understanding of how to to run the current project.

What could go wrong within a service? 👿

Example: Problems when Creating a New Reservation:

The first name and last name are required fields, as these are needed to generate part of the reservationId. If the first name or last name are missing, null or empty, then we cannot create a reservationId.

Secondly, the date and time of the reservation is also required, as otherwise we will not know when the customer will be arriving.

Just for the creation flow, there can be various things that the consumer of the API could do to cause our service to fail! We need to account for these, in order to provide the consumer of the API the best experience.

How Can We Resolve this? 🤔

When it comes to handling failure, we should try to be as clear as possible by providing the consumer of the API with some key information such as:

  • A custom error message (with no sensitive information)
  • An appropriate HTTP status code
  • The time at which the error occurred
  • The path which the client tried to interact with which failed.

Stick this into a JSON response, and we will have happier clients!

How Can Spring Boot Help Us?

Fortunately, Spring Boot provides us with this exact error response structure, which is great for when you are creating RESTful services.

{   
    "timestamp": 1610889198,
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/non-existing-endpoint-url"
}

We can easily modify this for our use cases, and can return back custom error responses / response status codes based exceptions thrown within our business logic.

Handling Create Reservation Problems

As a starting point, we can validate the incoming request objects, so that if the request does not meet some minimum requirements, we will immediately discard it. To do this, we can leverage a Spring Bean validation library.

Step 1: Add the Spring Validation Dependency to build.gradle
implementation 
'org.springframework.boot:spring-boot-starter-validation'

(At the time of writing this, the latest version is 7.0.0, however there may be newer versions available by now. See here for the latest version)

Step 2: Add Validation Constraints to the Reservation Domain object

We need to ensure that the firstName, lastName and reservationTime are required fields, therefore we can use the @NotBlank annotation.

In the Reservation DTO, modify the object as follows:

package com.rms.reservationservice.model;

import lombok.Builder;
import lombok.Data;

import javax.validation.constraints.FutureOrPresent;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Data
@Builder(toBuilder = true)
public class Reservation {
    private String id;

    @NotBlank(message = "First Name should not be empty")
    private String firstName;

    @NotBlank(message = "Last Name should not be empty")
    private String lastName;
    
    @NotNull
    @FutureOrPresent
    private LocalDateTime reservationTime;

    private int numberOfGuests;
    private int duration;

    //......
}
  • @NotBlank – checks that the input is not null, and the trimmed length of input is greater than 0. This means we can invalidate the request when the client enters an empty string as a firstName.
  • You can add in a message field, so we can give a more descriptive error response should the client fail to provide any required fields.
  • @NotNullUnfortunately, we cannot check for a “blank” date. Instead, we can check to see if the reservationTime exists
  • @FutureOrPresent – Reservation dates and times must be made for the future, as this is when a customer would be making it for.
Step 3: Validate the Incoming Request Body in the Controller

Though we have added the validation constraints to our model object, we still need to “activate” the bean validation. This is done by adding the @Valid annotation to the @RequestBody annotation in our Controller like so:

import javax.validation.Valid;

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Reservation save(@Valid @RequestBody Reservation reservation) {
    return reservationService.saveReservation(reservation);
}

If you start your application now and send through a POST request without the firstName parameter:

{
    "lastName":"Sapty",
    "reservationTime":"2007-12-03T10:15:30",
    "numberOfGuests": 2
}

The response will look something like this:

{
    "timestamp": "2021-01-17T15:39:46.477+00:00",
    "status": 400,
    "error": "Bad Request",
    "message": "",
    "path": "/api/v1/reservation"
}

Hmm…. Where are the validation messages? Let’s check the logs, maybe something is there…

2021-01-17 16:17:43.310  WARN 36673 --- [nio-8080-exec-1] 
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved 
[org.springframework.web.bind.MethodArgumentNotValidException:
 Validation failed for argument [0] in public 
com.rms.reservationservice.model.Reservation 
com.rms.reservationservice.controller.ReservationController.
save(com.rms.reservationservice.model.Reservation): 
[Field error in object 'reservation' on field 'firstName': 
rejected value [null]; 
codes [NotBlank.reservation.firstName,NotBlank.firstName,
NotBlank.java.lang.String,NotBlank];
 arguments [org.springframework.context.support
.DefaultMessageSourceResolvable: codes [reservation.firstName,firstName];
 arguments []; default message [firstName]];
 default message [First Name should not be empty]] ]

Okay, the logs are showing us that the validation has worked and that a

MethodArgumentNotValidException

was thrown. However it’s pretty damn ugly and also not in the JSON response! So what else is needed in order to have some graceful error responses?

Introducing the Controller Advice

One of the benefits of using Spring is that you can leverage the powers of Aspect Oriented Programming (AOP). To learn more about AOP, check out the docs here.

With AOP and ControllerAdvices, we can write global code and apply it to our controllers. Therefore, we can check to see when various exceptions are thrown and can create a global way to process these exceptions. This way, we can handle failure more gracefully and give the client a more readable failure response.

Step 4: Creating the ErrorResponse Object

As we would like our error response to be readable, we can create a ErrorResponse object, where we can define the keys of our error response.

Create a new package called exception. Next, create a new class called ErrorResponse which will contain our ErrorResponse template.

package com.rms.reservationservice.exception;

import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Map;

@Data
@Builder
public class ErrorResponse {
    private LocalDateTime timestamp;
    private Map<String, String> errors;
}

When the ErrorResponse object is serialised and returned back to the client, it will look something like this:

{
    "timestamp": "2021-01-17T18:21:03.821749",
    "errors": {
        "reservationTime": "must be a date in the present or in the future"
    }
}

Notice that this timestamp is now more readable than Epoch time, as well as an errors map. The errors map will contain all the fields which cause any problems for us. Along with this, we will define and provide a custom HTTP response code that we will choose very shortly.

Step 5: Creating the ControllerAdvice

Create a new class Custom Global Exception Handler class which is where we will implement our first exception handler.

package com.rms.reservationservice.exception;

import org.springframework.web.bind.annotation.ControllerAdvice;

@ControllerAdvice
public class CustomGlobalExceptionHandler {}
  • @ControllerAdvice – Indicates to Spring that this class is a ControllerAdvice and will allow you to perform exception handling techniques, applying them across the whole application.
Step 6: Implementing the @ExceptionHandler

Finally, we are ready to implement the exception handler and see some graceful error responses returned to the client!

As previously mentioned, when we attempted the request, we could see that a

MethodArgumentNotValidException

was thrown. As a result, we can catch this exception GLOBALLY, and therefore have some proper handling for it.

package com.rms.reservationservice.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
@ResponseBody
public class CustomGlobalExceptionHandler {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    protected ErrorResponse methodArgInValid(MethodArgumentNotValidException
 ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) ->
         {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .errors(errors)
                .build();
    }
}
  • @ResponseBody – In order for us to return the ErrorResponse back to the client in a JSON format, we need to provide this annotation. The Jackson library will take the object, and convert it back to JSON before returning this to the client.
  • @ExceptionHandler – This is used to define which exceptions we would like to catch, so that we can handle them ourselves. We can pass in various exception classes, as well as our own custom exception classes into the array, and they will all be handled in this exact same way!
  • @ResponseStatus – Just like in our Controller class where we provided this annotation, we can decide ourselves which HTTP status to return. In this case, as this is a client error, we can return back a 400 BAD REQUEST.
  • Method Argument Not Valid Exception – The exception that was thrown is passed into this method as a parameter, so that we can have access to its methods, and pull out all the fields that caused problems!

In the method body, we simply go through each of the errors that were thrown, and accordingly put these into a HashMap. The HashMap is made up of the field name, as well as the constraint message that we defined in our Reservation model object. Finally, we attach the timestamp to the response and return this back to the client.

TIME TO TEST!

We can boot up our application and in our Create request, let’s pass in an in an invalid request and see the response that comes back to us:

curl -X POST -H "Content-Type: application/json" \
 -d '{
    "lastName":"Sapty",
    "reservationTime":"2031-12-03T10:15:30",
    "numberOfGuests": 2
 }' http://localhost:8080/api/v1/reservation

Response:

{
    "timestamp": "2021-01-27T20:50:50.431168",
    "errors": {
        "firstName": "First Name should not be empty"
    }
}

If you did get stuck at any point, please checkout the branch part-1-implementing-bean-validation and compare your code against mine.

GREAT! 🥳 We have implemented validation around the save method… What about the others?!

Okay, now that the hard stuff is out of the way, we can focus on finishing the other three controller methods. If you remember, the Read, Update and Delete methods all have one thing in common – they all pass in a reservationId as a @PathVariable. Without the reservationId, we simply cannot perform any read, update or delete operations for a given reservation.

We know that a reservationId take the form ABCD-12345, which is made up of 4 letters, a dash and 5 numbers. As a result, when applying validation to our @PathVariable, we can attach certain constraints onto this as well to ensure that the reservationId meets this requirement.

For example, we can use the @NotBalnk and @Size(min = 8, max = 8) annotation which can provide us some validation for the incoming request. However, this still does not stop the client from putting in any 8 character String. As our reservationId follows a format of ABCD-12345, we can instead create our own custom validator which can check to see if the reservationId follows the correct format!

Wait, We Can Do That?! 🤯

Of course, we create our own validator which can check the format of the string by using regular expression validation. Then we can apply this to all incoming requests that pass in a reservationId as a path variable.

First, we need to create a new interface that is going to be used to create our own custom annotation. Once it’s created, we will need to create an implementation class to perform the validation. Finally, once this is all done, we can then apply the validation annotation onto our path variable parameters.

Step 1: Come up with a new validation annotation

Create a new package called validation. Then create a new interface called ValidateID which will look a little different to what you would expect!

package com.rms.reservationservice.validation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = ReservationIdValidator.class)
@Documented
public @interface ValidateID {
    String message() default "ReservationId is invalid.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Okay… WTF is going on here bro…🧐

  • @Target ({PARAMETER}) – Indicates that ValidateID will be used on within the parameter of a method only. (See more here). We are going to be making our own custom annotation! 🤗
  • @Retention (RUNTIME) – Indicates that this annotation will only be used during runtime. (see more here).
  • @Constraint – Indicates that ValidateID will be a constraint, and therefore must implement the message, groups and payload methods. It also means that this can throw a
ConstraintViolationException  (which we will look at later)
  • We are also saying that we will provide a validation class which will determine if the constraint in question is valid or not.
  • @interface – This is used as a marker to indicate that we would like to create a new annotation.
The New Annotation Interface Body

Unlike a normal interface, rather than providing methods to implement, we are providing attributes that can be inserted into the annotation as parameters. e.g.

@ValidateID(message="hellllooooo world!") 
  • message()– provides some default message should something go wrong, should we not provide our own custom message.
  • groups()- Defines which circumstances this validation is to be triggered.
  • payload() – Defines the payload to be passed with this validation.
Step 2: Implement the validation class.

Now that we have created the annotation, we need to provide the ReservationIdValidator class which is what is going to determine whether or not the reservationId is valid or not.

Create a new class in the validation package called ReservationIdValidator. Then Implement the ConstraintValidator interface and we are ready to move on

package com.rms.reservationservice.validation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class ReservationIdValidator 
implements ConstraintValidator<ValidateID, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        //implement our own custom validation for the reservation Id.
        return false;
    }
}

In the ReservationIdValidator class, we implement the ConstraintValidator interface. This takes the annotation that we want to use, as well as the item that we want to validate against. In this case, it is a reservationId which is of type string.

In the next section, we going to implement the painful regex to get this validation working!

If you did get stuck at any point, please checkout the branch part-1-creating-custom-validator-interface and compare your code against mine.

Step 3: Implement the validation logic

Great, this is where the validation code is going to go, so that we will only process a request if it follows the pattern ABCD-12345.

package com.rms.reservationservice.validation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ReservationIdValidator
 implements ConstraintValidator<ValidateID, String> {

    @Override
    public boolean isValid(String value,
 ConstraintValidatorContext context) {

        Pattern pattern = Pattern
            .compile("([A-Z]){4}[-][1-9]{5}");
        Matcher matcher = pattern.matcher(value.toUpperCase());
        try {
            return matcher.matches();
        } catch (Exception e) {
            return false;
        }
    }
}

Fortunately, the reservationId regular expression validation is somewhat straightforward. We first compile the regular expression which we seek – 4 characters, a dash, and 5 numbers from 1-9. We then match the pattern against our input and see if it is a match Should anything go wrong e.g. the user provide a bad input, then the exception would be caught and the request is invalidated.

Finally, the last thing that needs to be completed, is to apply the new annotation within our Controller layer.

Step 4: Apply the newly created annotation!

Now that we have created the logic, we only have a couple of things left to go, and then we are done!

Back in the ReservationController class, we can now start adding the new @ValidateId annotation as part of our input parameters, just like how we used @PathVariable. For example:

@GetMapping("/{reservationId}")
@ResponseStatus(HttpStatus.OK)
public Reservation get(@PathVariable("reservationId") 
@ValidateID String reservationId) {
    return reservationService.getReservationById(reservationId);
}

Similarly, we can apply this to the Update and Delete methods too! However, even after doing this, if you were to boot up your application, the new custom validation will still not work 😒.

🤬 CAN YOU STOP TEASING US AND SHOW US HOW IT WORKS 🤬

When working with path variables, we need to explicitly tell Spring to validate our method parameters, which is how the new annotation will take effect!

package com.rms.reservationservice.controller;

import org.springframework.validation.annotation.Validated;
//other imports and annotations already in the class
@Validated
public class ReservationController {
   //constrcutor and create method already exist.

    @GetMapping("/{reservationId}")
    @ResponseStatus(HttpStatus.OK)
    public Reservation get(@PathVariable("reservationId") 
@ValidateID String reservationId) {
        return reservationService.getReservationById(reservationId);
    }

    @PutMapping("/{reservationId}")
    @ResponseStatus(HttpStatus.OK)
    public Reservation update(@PathVariable("reservationId")
 @ValidateID String reservationId, @RequestBody Reservation reservation) {
        return reservationService.updateReservation(reservationId, 
reservation);
    }
    @DeleteMapping("/{reservationId}")
    @ResponseStatus(HttpStatus.OK)
    public void delete(@PathVariable("reservationId") 
@ValidateID String reservationId) {
        reservationService.deleteReservation(reservationId);
    }
}
  • @Validated – This is what tells Spring to actively evaluate constraints that exist in our method parameters. This means we are telling Spring to validate our reservationId when it is passed into the controller.

Should the validation fail, (and please so try this), you will probably see a response like this:

{
    "timestamp": "2021-01-17T23:33:16.380+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "/api/v1/reservation/CLSY-917"
}

And in the logs, we will now see a big, pretty intimidating stack trace as your service has not been able to handle the validation failure! Looking at the stack trace, we can see one very important line (right at the top):

javax.validation.ConstraintViolationException: 
delete.reservationId: ReservationId is invalid.

So we can see clearly, that the @ValidateID was invoked, as it returned back to us the default message, but with an ugly stack trace to go with it! So how can we fix this?

If you did get stuck at any point, please checkout the branch part-1-implementing-custom-reservation-id-validation-logic and compare your code against mine.

Step 5: Making our Custom Global Exception Handler work its magic!

Back in our global exception handler class, we currently only handle for Method Argument Not Valid Exception‘s. However, now that we have multiple methods trying to validate a reservationID, it makes sense to have a common exception handler to handle the new exception that can be thrown:

ConstraintViolationException

Therefore, we can create a new method within here:

package com.rms.reservationservice.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.validation.ConstraintViolationException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
@ResponseBody
public class CustomGlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    protected ErrorResponse handleMethodArgumentNotValid(
        MethodArgumentNotValidException ex) {
    //...
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    protected ErrorResponse 
       handleConstraintViolated(ConstraintViolationException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getConstraintViolations().forEach(constraintViolation -> {
            String message = constraintViolation.getMessage();
            String propertyPath = constraintViolation
        .getPropertyPath()
        .toString();
            errors.put(propertyPath, message);
        });
        return ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .errors(errors)
                .build();
    }
}

Similarly, we iterate over the validation errors, put them into our ErrorResponse object, and this is then returned back to the client in a graceful way.

TIME TO TEST!

We can boot up our application and in our Delete request, let’s pass in an in an invalid reservationId format and see the response that comes back to us:

curl -X DELETE http://localhost:8080/api/v1/reservation/CLSY-917
{
    "timestamp": "2021-01-27T23:01:09.675507",
    "errors": {
        "delete.reservationId": "ReservationId is invalid."
    }
}

We can see here, that the response that has come back is much more useful for the client, as it clearly indicated that the reservationId is invalid. In a similar vein, you can now go ahead and test this for the read and update methods where you have applied the new @ValidateID annotation!

If you did get stuck at any point, please checkout the branch part-1-completing-custom-annotation-validation and compare your code against mine.

You made to the end of Part ! Awesome Job!🎉🎉🎉🎉

Bean validation is just one of the many things we can do start making our services fail more gracefully. By performing this kind of validation on the incoming JSON object, we can prevent invalid requests entering our service.
But you may be wondering, what if the request is valid, but the actual data within the request is not correct? What if the reservationId is in a valid format, but does not exist in the database?! THEN WHAT!? 😱

Fortunately for you, we are going to be covering this exact type of scenario in Part 2 of this series! See you there! 😎