Bean validation is a common requirement in daily development. This article describes how to validate beans with Spring Validation Stater. The full code can be downloaded here.

Introduce the Spring Boot Validation Starter

Spring-boot-starter-validation is introduced to quickly load validation dependencies.

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

Basic knowledge of

The basic way Bean Validation works is to define constraints on a class’s fields by annotating them with some Annotation.

Common validation annotations

The following are commonly used validation annotations, and more comprehensive annotations are available here.

  • @notnull: The field cannot be null.
  • @notempty: Indicates that the list field cannot be empty.
  • NotBlank: indicates that a string field cannot be an empty string (that is, it must have at least one character).
  • @min and @max: indicates that a numeric field is valid only if its value is above or below a certain value.
  • @pattern: indicates that the string field is valid only if it matches a regular expression.
  • @email: Indicates that the string field must be a valid Email address.

Here is a code example.

public class Input {

  @NotBlank
  private String id;

  @Email
  private String email;

  @Min(1)
  @Max(91)
  private int age;

  @pattern (regexp = "^[A-za-z0-9]{8,16}$",message = "the user name must be a string of 8 to 16" +" characters containing numbers and upper and lower case letters ")
  privateString userName; . }Copy the code

The Validator object Validator

The Validator object Validator verifies that an object is valid by passing the object to the Validator to check that the constraints can be met.

Set<ConstraintViolation<Input>> violations = validator.validate(customer);
if(! violations.isEmpty()) {throw new ConstraintViolationException(violations);
}
Copy the code

@Validated and @Valid

In Spring, instead of creating a validator object ourselves, we use the @validated and @valid annotations to tell Spring that we want to validate an object, and Spring automatically validates it for us.

  • @validated is a class-level annotation that we use to tell Spring to validate the parameters passed to the method in which the annotated class is added.
  • The @valid annotation is added to method arguments and fields to tell Spring that we want to validate method arguments or fields.

Validate the input from the Spring MVC controller

Suppose we have implemented a Spring REST controller and want to validate the input. For any incoming HTTP request, we can validate three types of request data:

  • RequestBody, @requestbody
  • Path variables such as /order/{orderId}
  • Query parameters, parameters of GET requests

Request experience certificate

In POST or PUT requests, JSON data is typically passed in the request body. Spring automatically maps the incoming JSON to Java objects, and we need to check that the incoming Java objects meet our requirements.

So let’s say that this is our incoming object.

public class Input {

  // Id cannot be null
  @NotBlank
  private String id;

  // An email address
  @Email
  private String email;

  // The minimum age is 1 and the maximum age is 91
  @Min(1)
  @Max(91)
  private int age;

  // The user name contains 8 to 16 digits and uppercase and lowercase letters
  @pattern (regexp = "^[A-za-z0-9]{8,16}$",message = "the user name must be a string of 8 to 16" +" characters containing numbers and upper and lower case letters ")
  privateString userName; . }Copy the code

To validate the body of the incoming HTTP request, we annotate the request body with @valid in the REST controller:

@RestController
public class ValidateRequestBodyController {

  @PostMapping("/validateBody")
  ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {
    return ResponseEntity.ok("valid"); }}Copy the code

We simply added the @valid annotation to the input parameter, which is also annotated with @requestbody to indicate that it should be read from the RequestBody. This way Spring passes the object to the validator for validation before doing anything else. If the validation fails, it will trigger MethodArgumentNotValidException. By default, Spring converts this exception to HTTP status 400 (error request).

Can test case by running the code com example. Springbootdemo. Controller. ValidateRequestBodyControllerTest to see the effect.

Path variables and query parameter validation

Validating path variables and request parameters work slightly differently.

Instead of validating complex Java objects, annotations (@min in this case) are added directly to the method parameters in the Spring controller, since path variables and request parameters are basic types like int.

@RestController
@Validated
class ValidateParametersController {

  @GetMapping("/validatePathVariable/{id}")
  ResponseEntity<String> validatePathVariable(
      @PathVariable("id") @Min(5) int id) {
    return ResponseEntity.ok("valid");
  }
  
  @GetMapping("/validateRequestParameter")
  ResponseEntity<String> validateRequestParameter(
      @RequestParam("param") @Min(5) int param) { 
    return ResponseEntity.ok("valid"); }}Copy the code

Note that you must add Spring’s @Validated annotation to the class so that Spring performs the constraint annotation on method parameters in the class. : : :

And request to experience the different failed validation would trigger ConstraintViolationException here, rather than MethodArgumentNotValidException. By default, Spring does not register a default exception handler for this exception, which results in a response with HTTP status 500 (internal server error).

If we want to return HTTP status 400 (which makes sense because the client provided an invalid parameter, making it an error request), we can add a custom exception handler to our Controller:

@RestController
@Validated
class ValidateParametersController {

  // request mapping method omitted
  
  @ExceptionHandler(ConstraintViolationException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
    return new ResponseEntity<>("not valid due to validation error: "+ e.getMessage(), HttpStatus.BAD_REQUEST); }}Copy the code

Can test case by running the code com example. Springbootdemo. Controller. ValidateParametersControllerTest to see the effect.

:::info Later, we’ll see how to return a structured error response that contains details of all failed validations for the client to check. : : :

Validate the input to the Spring-@service method

We can also validate the input of any Spring component by using the @validated and @valid annotations in combination:

@Service
@Validated
class ValidatingService {

  void validateInput(@Valid Input input) {
    // do something}}Copy the code

Again, the @validated annotation is only at the class level; do not place it on methods in this use case. Here is a test case to verify this method:

@ExtendWith(SpringExtension.class)
@SpringBootTest
class ValidatingServiceTest {

  @Autowired
  private ValidatingService service;

  @Test
  void whenInputIsInvalid_thenThrowsException(a){ Input input = invalidInput(); assertThrows(ConstraintViolationException.class, () -> { service.validateInput(input); }); }}Copy the code

Spring Boot custom validator

If the default constraint annotation is not sufficient for our requirements, we can create our own constraint annotation and customize a validator to meet our requirements.

For example, we have fields called gender and ipAddress. The value of gender can only be a string of ‘F’ or ‘M’, and IP must comply with the rules of IP.

Let’s start with gender annotations and validators:

@Documented
@Constraint(validatedBy = {EnumStringValidator.class}) // Specify EnumStringValidator to validate
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumString {
  String message(a) default "value not invalid"; Class<? >[] groups()default{}; Class<? extends Payload>[] payload()default{};// Used to set the range of values that the validator will use
  String[] values();
}
Copy the code

Custom constraint annotations need to satisfy all of the following conditions:

  • Parameter message message(), which is used to prompt a message in case of a conflict,
  • Parameter groups(), which allows you to define under which circumstances this validation is triggered (we’ll discuss validation groups later),
  • Payload, very rarely used, and you’re not being discussed here
  • @constraint annotation pointing to the ConstraintValidator interface implementation.

Next, look at the validator.

public class EnumStringValidator implements
    ConstraintValidator<EnumString.String> {
  
  private List<String> list;
  // Initialize according to values set in @enumString
  @Override
  public void initialize(EnumString constraintAnnotation) {
    list = Arrays.asList(constraintAnnotation.values());
  }
  // Check whether the configuration conditions are met
  @Override
  public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
    if (value == null) {
      return false;
    }
    returnlist.contains(value); }}Copy the code

@ipAddress and IpAddressValidator see the code.

Next, we can use the @EnumString and @ipAddress annotations just like any other constraint annotations.

Use groups for different validation schemes for objects in different use cases

In development, the same class can appear in multiple scenarios, taking a typical CRUD operation as an example: Both the “create” and “update” use cases are likely to take the same object type as input. However, in different scenarios, the constraints on the fields in the instance are not exactly the same. We can set groups() for validation annotations and set different validation rules for the same class.

Notice that all constraint annotations must have a Groups field. This can be used to pass any class, each of which defines a specific validation group that should fire.

For the CRUD example, we simply define two tag interfaces OnCreate and OnUpdate:

interface OnCreate {}

interface OnUpdate {}
Copy the code

We can then use these tag interfaces with any constraint annotations, as follows:

public class InputWithCustomValidator {

  @EnumString(values = {"F", "M"}, groups = OnCreate.class)
  private String gender;

  @IpAddress(groups = OnUpdate.class)
  privateString ipAddress; .Copy the code

In the InputWithCustomValidator class, the gender attribute is validated only in the OnCreate group and the ipAddress is validated only in the OnUpdate group.

Spring supports verification groups through the @Validated annotation.

@Service
@Validated
class ValidatingServiceWithGroups {

  @Validated(OnCreate.class)
  void validateForCreate(@Valid InputWithCustomValidator input) {
    // do something
  }

  @Validated(OnUpdate.class)
  void validateForUpdate(@Valid InputWithCustomValidator input) {
    // do something}}Copy the code

Note that the @Validated annotation must be used for the entire class. To define which validation group should be active, you must also apply it at the method level. : : :

By executing the test cases com. Example. Springbootdemo. Service. ValidatingServiceWithGroupsTest, to see the effect.

Handling validation errors

When validation fails, we want to return a meaningful error message to the client. To enable the client to display useful error messages, we should return a data structure containing error messages for each failed validation.

First, we need to define the data structure. We’ll call this ValidationErrorResponse and it contains a list of conflicting objects:

public class ValidationErrorResponse {

  private List<Violation> violations = new ArrayList<>();

  // ...
}

public class Violation {

  private final String fieldName;

  private final String message;

  // ...
}
Copy the code

Then, we create a global ControllerAdvice, it handles all appear at the Controller level ConstraintViolationException. In order to capture the request body validation errors, we will also deal with MethodArgumentNotValidException.

@ControllerAdvice
class ErrorHandlingControllerAdvice {

  @ExceptionHandler(ConstraintViolationException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  ValidationErrorResponse onConstraintValidationException( ConstraintViolationException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (ConstraintViolation violation : e.getConstraintViolations()) {
      error.getViolations().add(
        new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
    }
    return error;
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  ValidationErrorResponse onMethodArgumentNotValidException( MethodArgumentNotValidException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
      error.getViolations().add(
        new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
    }
    returnerror; }}Copy the code

Here we read information about the violation from the exception and convert it to a ValidationErrorResponse data structure.

:::caution Note the @ControllerAdvice annotation, which makes the exception handler methods globally available to all controllers in the application context. : : :

conclusion

In this tutorial, we showed you how to use Spring Boot for validation. The code can be downloaded here.