Spring boot bean validation

In this article, we will take a look at applying validations directly on bean fields in spring boot.
Validation is required so that invalid values are not submitted for processing and the request is returned back to the caller, if it does not contain values in required format.
This is also called server side validation.

Overview

Validation in java or spring applications is based on Jakarta bean validation or JSR-303.
It allows you to apply validations on fields of a java class using annotations.

Validation is applied over the incoming request before it arrives at the controller. If the validation fails, HTTP 400 error is returned.
There are methods to handle this so that custom error response is returned.

Common annotations used for applying validations are
@NotNull
Validates if the field with this annotation is not null.
@Null
Validates and ensures that the field with this annotation is null.
@NotEmpty
Validates and ensures that the value of this field is not blank.
@Pattern
Tests the field value against a pattern.

These and other annotations are given at the end.

Configuration

As a first step to apply bean validations in Spring boot, add spring boot starter validation dependency in your project as per the build tool.

// GRADLE
implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.0'

// MAVEN
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.7.0</version>
</dependency>

Set Up

Suppose below is a java class or bean or POJO on whose fields, we need to apply validation

import lombok.Data;

@Data
public class Post {
 
  private Integer id;
  
  private String title;
  
  private String body;
  
  private String userId;

}

Here @Data is an annotation from Project Lombok, which generates a constructor, getters for all fields, hashCode() and toString() methods. This has nothing to do with bean validation and its not necessary for validations to work.

Below is a Spring Rest Controller or a Controller that contains a method accepting this object as argument

@RestController
public class PostController {

  @PostMapping("/post)
  public void savePost(Post p) {
    // code
  }

}

Applying validations

Following are the field validations that we want to apply on the post object in the incoming request.
1. It must have id, title and body fields.
2. title field should have at least 10 characters and at most 25 characters.
3. body should be of at least 20 characters.
4. email should be valid.

With spring boot validation starter in place, we can directly apply annotations over the fields of Post class corresponding to the required validations.

Updated Post class will be

import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;

import org.hibernate.validator.constraints.Length;

import lombok.Data;

@Data
public class Post {

  @NotNull
  private Integer id;
  
  @NotNull
  @Length(min = 10, max = 25)
  private String title;
  
  @NotNull
  @Length(min = 20)
  private String body;
  
  @Email
  private String userId;

}

Annotations are self explanatory.

Second step is to inform Spring boot to validate the incoming object against these annotations.
This is achieved by applying @Valid annotation in controller which accepts the post object as shown below

@PostMapping("post")
public void savePost(@Valid @RequestBody Post p) {
  // code
}

Now if a request is sent with any invalid values in request body such as below

{
    "id":1,
    "title":"Dummy",
    "body":"Dummy body",
    "userId":"test"
}

You will get an HTTP 400 Bad Request error.
This is an indication that there is something not right about the request body.

Format of response received will be

{
    "timestamp": "2022-06-07T15:18:39.447+00:00",
    "status": 400,
    "error": "Bad Request",
    "path": "/post"
}

which does not look good and it also does not exactly indicate where the problem is.

Validation error messages

In order to return meaningful validation errors, make use of Spring exception handling mechanism with @ExceptionHandler.

Add below method in the controller class.

@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleException(MethodArgumentNotValidException e) {
  List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
  Map<String, String> response = new HashMap<>();
  allErrors.forEach(v -> {
    String fieldName = ((FieldError) v).getField();
    response.put(fieldName, v.getDefaultMessage());
  });
  return response;
}

Explanation of this method can be broken down into following points.
1. A method annotated with @ExceptionHandler followed by an exception class indicates that any exception of this type will be handled by this method.

2. When a Spring bean validation fails, it throws an exception of type MethodArgumentNotValidException.
Spring docs for this exception state

Exception to be thrown when validation on an argument annotated with @Valid fails.

So, when validation will fail, this method will be invoked.

3. All validation errors contained in this exception can be retrieved by calling getBindingResult() followed by getAllErrors().

4. Each error object contains the name of field whose validation failed and the reason of failure.

5. Field name and validation failure reason are placed in a map in the form of key-value pairs and it is returned.

6. A desired response code is also returned through @ResponseStatus annotation applied over the method.

Now, if the same request is sent again, we receive below response

{
  "title": "length must be between 10 and 25",
  "body": "length must be between 20 and 2147483647",
  "userId": "must be a well-formed email address"
}

This is very clear and indicates the exact reasons of failure.

If you are using a dedicated exception handler class having @ControllerAdvice annotation, then do not forget to apply @ResponseBody annotation before handler method.

Accepting request

If Spring bean validation fails, then the request will never reach the controller. It will be returned beforehand.
There are scenarios where you want the request to reach controller even when the validation fails. Probably for applying custom validations on the top or persisting received request to a database.

To achieve this, add a parameter of type org.springframework.validation.BindingResult to the controller method.
Check if there are any validation errors using its hasErrors() method.
If it returns true, then get all errors using its getAllErrors() method as shown in previous section. Example,

@PostMapping("post") 
public void savePost(@Valid @RequestBody Post p,
                    BindingResult r) { 
  if(r.hasErrors()) {
    List<ObjectError> allErrors = r.getAllErrors();
  }
  // code 
}

Remember that when BindingResult argument is added to the controller method, the exception handler method will not be invoked, even if the validation fails.

Spring boot validations list

Below is the list of commonly used validations from Jakarta bean validation specification.

@AssertTrue
Applied over a boolean field. Validates if its value is true in the incoming request.

@AssertFalse
Validates if the field value is false in the incoming request.

@Digits
Applicable to numeric types. Validates if the value is within given range.

@Future
Applied over a date/time field such as
java.util.Date,
java.util.Timestamp
,
java.time.LocalDate
,
java.time.LocalDateTime etc.
Validates if its value is lies in future.

@Email
Validates if the string value is a valid email.

@FutureOrPresent
Same as above except that it also validates present instant.

@Min
Applicable to number types such as BigInteger, BigDecimal, long, int and short.
Validates if the request value is higher than the specified value.

@Max
Validates if the request value is smaller than the specified value.

@NotBlank
Validates if the request value is not empty.

@Null
Validates that the request value is null.

@NotNull
Validates that the request value should be not null.

@Pattern
Applied over a string and it should match the regex provided.

There are many more. Here is a detailed list.