Validating Data in Spring Boot applications

A good service always validates its data before processing it. In this tutorial, we will look at the Bean Validation API and use its reference implementation in a Spring Boot application.

Important: Until Spring Boot version 2.2 the starter spring-boot-starter-web had as dependency the starter spring-boot-starter-validation. In Spring Boot 2.3 the starter spring-boot-starter-validation is NOT a dependency of the starter spring-boot-starter-web anymore so you need to add explicitly. the spring-boot-starter-validation:

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

The Bean Validation API provides a number of annotations that can be used to validate beans.

Creating validations involves two steps:

1. Enabling validation on the controller method.

2. Adding validations on the bean.

Validating the Spring Boot Controller

The simplest way to validate data is to include Constraints om the Controller methods. See this example:

@Component
public class CustomerRepository {
  List<Customer> customerList = new ArrayList<Customer>();

  @PostConstruct
  public void init() {
    customerList.add(new Customer(1, "frank", "USA"));
    customerList.add(new Customer(2, "john", "USA"));
  }

  public List<Customer> getData() {
    return customerList;
  }

  public Customer save(Customer c) {
    customerList.add(c);
    return c;
  }
}

And here is the Controller Class:

@Validated
@RestController
public class CustomerController {
  @Autowired CustomerRepository repository;

  @RequestMapping("/list")
  public List<Customer> findAll() {
    return repository.getData();
  }

  @RequestMapping("/find/{id}")
  public Customer findOne(@PathVariable @Min(1) @Max(3) int id) {
    return repository.getData().get(id);
  }
}

As you can see, the Controller has the @Min and @Max constraint which will check that we are searching a valid id for the customer.

Also, the @Validated annotation is evaluated on class level in this case, even though it’s allowed to be used on methods.

If we run the above example, the following error will be reported:

curl -s http://localhost:8080/find/5 | jq {   "timestamp": "2020-04-02T07:39:58.172+0000",   "status": 500,   "error": "Internal Server Error",   "message": "findOne.id: must be less than or equal to 3",   "path": "/find/5" } 

If we want to return a HTTP status 400 instead (which makes sense, since the client provided an invalid parameter, making it a bad request), we can add a GlobalExceptionHandler to our contoller:

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
  @ExceptionHandler(ConstraintViolationException.class)
  public void constraintViolationException(HttpServletResponse response) throws IOException {
    response.sendError(HttpStatus.BAD_REQUEST.value());
  }
}

Now execute again the request and you will get a “Bad Request” error to indicate that the Request was not properly composed:

curl -s http://localhost:8080/find/5 | jq {   "timestamp": "2020-04-02T07:40:44.739+0000",   "status": 400,   "error": "Bad Request",   "message": "findOne.id: must be less than or equal to 3",   "path": "/find/5" } 

Validating the Bean Data

Validation can also be placed on the Bean data.

Validating the Bean Data can turn into an antipattern! You should however consider that if we only validate in the Bean layer, we accept the risk that the web and business layer work with invalid data. Invalid data may lead to severe errors in the business layer therefore validation in the Bean or persistence layer can then act as an additional safety net, but not as the only place for validation.

The following Constraints have been placed in our Customer Bean:

public class Customer {
  private int id;
  @Size(min = 5, message = "Enter at least 5 Characters.") private String name;
  @Country private String country;
  public Customer(int id, String name, String country) {
    super();
    this.id = id;
    this.name = name;
    this.country = country;
  }

The above constraints will be verified as we attempt to persist one Entity:

@PostMapping("/save") public Customer newCustomer(@Valid @RequestBody Customer customer) {
  return repository.save(customer);
}

Additionally, we will add to our GlobalExceptionHandler the following method to provide a meaningful response in case the input data is invalid:

@Override protected ResponseEntity < Object > handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
  Map < String, Object > body = new LinkedHashMap < > ();
  body.put("timestamp", new Date());
  body.put("status", status.value());
  //Get all errors
  List < String > errors = ex.getBindingResult().getFieldErrors().stream().map(x -> x.getDefaultMessage()).collect(Collectors.toList());
  body.put("errors", errors);
  return new ResponseEntity < > (body, headers, status);
}

If we try to save a new Customer which fails the validation, the following error will be returned:

 curl -v -X POST localhost:8080/save -H "Content-type:application/json" -d "{\"name\":\"Carl\", \"country\":\"USA\"}"   {"timestamp":"2020-04-02T09:26:04.546+0000","status":400,"errors":["Enter at least 5 Characters."]}  

It is worth to note, that the following Validation Constraints can be used:

  • @AssertFalse : Checks for false. @Assert checks for true.
  • @Future : The annotated element must be a date in the future.
  • @Past : The annotated element must be a date in the past.
  • @Max : The annotated element must be a number whose value must be lower
  • @Min : The annotated element must be a number whose value must be higher
  • @NotNull : The annotated element cannot be null.
  • @Pattern : The annotated {@code CharSequence} element must match the specified regular expression. The regular expression follows the Java regular expression conventions.
  • @Size : The annotated element size must be within the specified boundaries.

Using a Custom Validator

If you aren’t satisfied with the default Constraints which are available, you can create custom ones and place them in your code. For example, let’s write a custom Constraint which will check the Country of the Customer Bean, allowing only customers from “USA” or “Canada”. In order to do that, let’s add the CountryValidator which implements ConstraintValidator

public class CountryValidator implements ConstraintValidator<Country, String> {
  List<String> authors = Arrays.asList("USA", "Canada");

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    return authors.contains(value);
  }
}

To compile that, we need to the Country interface:

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = CountryValidator.class)
@Documented
public @interface Country {
  String message() default "We only accept customers from USA and Canada";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};
}

With that in place, if we try to save a Customer with a not valid Country, it will display the following error:

curl -v -X POST localhost:8080/save -H "Content-type:application/json" -d "{\"name\":\"Federick\", \"country\":\"Germany\"}" {"timestamp":"2020-04-02T08:01:16.205+0000","status":400,"errors":["We only accept customers from USA and Canada"]} 

Validation input data in Web applications

To see how Validation works with input data in a simple MVC application, let’s take the example from this tutorial: How to create a Spring Boot CRUD application with H2 Database

Here, the Customer data can be added in the front-end layer as follows:

<form action="#" th:action="@{/addcustomer}" th:object="${customer}" method="post">
	<div class="row">
		<div >
			<label for="name">Name</label>
			<input type="text" th:field="*{name}" id="name" placeholder="Name">
				<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" ></span>
			</div>
			<div >
				<label for="surname" class="col-form-label">Surname</label>
				<input type="text" th:field="*{surname}"  id="surname" placeholder="Surname">
					<span th:if="${#fields.hasErrors('surname')}" th:errors="*{surname}" ></span>
				</div>
				<div >
					<label for="email" class="col-form-label">Email</label>
					<input type="text" th:field="*{email}"  id="email" placeholder="Email">
						<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" ></span>
					</div>
				</div>
				<div class="row">
					<div >
						<input type="submit"  value="Add Customer">
						</div>
					</div>
				</form>

The Customer Bean contains the following Constraints:

@Entity public class Customer {
  @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id;
  @NotBlank(message = "Name is required") private String name;
  @Size(min = 3, message = "Surname requires at least 3 characters.") private String surname;
  @Pattern(regexp = ".+@.+..+", message = "Please provide a valid email address") private String email;
  public Customer() {}
  public Customer(String name, String email) {
    this.name = name;
    this.email = email;
  }
  // Getter / Setters here  
}

As a result, if you try to insert a Customer which is not valid, th following error will be printed directly in the input form:

spring boot validation tutorial

Source code for this tutorial: https://github.com/fmarchioni/masterspringboot/tree/master/rest/demo-validation