Complete Guide to Spring Boot Validation

Spring Boot offers comprehensive support for Request validation using the Bean Validation specification. With Spring Boot, you can quickly add validation to your existing classes with minimal effort. We’ll also take a look at how we can use validation groups to invoke different validations in different use cases.

We will also show you how to use the validation starter to include validation in your project.

Validated requests for data make your life easier. You can be more confident that the data you are receiving is exactly what you expect it to be.

Spring Boot auto configures validation feature supported by Bean Validation as long as a JSR-303 implementation (such as Hibernate validator) is on the class path.

Built-in constraints

Hibernate Validator comes with a standard set of validators. The Jakarta Bean Validation standard defines the first set of validations. . Additionally, Hibernate Validator also provides useful custom constraints.

Jakarta Bean Validation constraints

@AssertFalse,@AssertTrue@Future@NotBlank@NegativeOrZero@Pattern
@DecimalMax,@DecimalMax@FutureOrPresent@NotEmpty@Null@Positive
@Digits@Max(value=)@NotNull@Past@PositiveOrZero
@Email@Min(value=)@Negative@PastOrPresent@Size(min=, max=)

Additional constraints

In addition to the constraints defined by the Jakarta Bean Validation API, Hibernate Validator provides
several useful custom constraints which are listed below

@CreditCardNumber@ISBN@Mod11Check@URL@PESEL
@Currency@Length(min=, max=)@Normalized@CNPJ@REGON
@DurationMax@CodePointLength@Range@CPF@INN
@DurationMin@LuhnCheck@ScriptAssert@TituloEleitoral
@EAN@Mod10Check@UniqueElements@NIP

You can find more information about built-in validators from the official documentation

Add Dependency

We need to add following dependency to spring boot project to auto configure validation feature in Spring Boot application.

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

Note

In all Spring Boot versions below version 2.2, Validations starter is part of web starter project

@Valid Annotation

When Spring Boot finds a method argument annotated with @Valid,it automatically bootstraps the Hibernate Validators and validates the argument. When target argument fails to pass the validation, Spring Boot throws MethodArgumentNotValidException

Validating Requests in Spring Boot Applications

Validating Request Body

I am going to use the REST API developed in one of my previous post to showcase the spring boot validation feature.

Annotate method parameter with @Valid annotation in your controller class.

@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    EmployeeService employeeService;

   
 ...  
    @PostMapping  
    public Employee createEmployee(@RequestBody @Valid Employee employee) {       
        return employeeService.createEmployee(employee);
    }

    @PutMapping()
    public Employee updateEmployee(@RequestBody @Valid Employee employee) {
        return employeeService.updateEmployee(employee);
    }
   ...
}

Now write the validation annotations on the bean.

@Entity
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @NotBlank(message = "FirstName should not be blank")
    @Size(min = 3,message = "FirstName should be at least 3 chars")
    private String first_name;

    @NotBlank(message = "LastName should not be blank")
    @Size(min = 3,message = "LastName should be at least 3 chars")
    private String last_name;

    @Enumerated(EnumType.STRING)
    private Gender gender;

    @PastOrPresent
    private LocalDate birth_date;

    @PastOrPresent
    private LocalDate hire_date;

    @NotBlank 
    @Email   
    private String email;

    @PositiveOrZero
    private BigDecimal salary;

    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;



    public Employee() {

    }
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getFirst_name() {
        return first_name;
    }

    public void setFirst_name(String first_name) {
        this.first_name = first_name;
    }

    public String getLast_name() {
        return last_name;
    }

    public void setLast_name(String last_name) {
        this.last_name = last_name;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    public LocalDate getBirth_date() {
        return birth_date;
    }

    public void setBirth_date(LocalDate birth_date) {
        this.birth_date = birth_date;
    }

    public LocalDate getHire_date() {
        return hire_date;
    }

    public void setHire_date(LocalDate hire_date) {
        this.hire_date = hire_date;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public BigDecimal getSalary() {
        return salary;
    }

    public void setSalary(BigDecimal salary) {
        this.salary = salary;
    }

    public Department getDepartment() {
        return department;
    }

    public void setDepartment(Department department) {
        this.department = department;
    }



}

Now let’s test the validation by sending a invalid request.

The complete response looks like below.

{
    "timestamp": "2022-05-08T13:31:09.303+00:00",
    "status": 400,
    "error": "Bad Request",
    "message": "Validation failed for object='employee'. Error count: 2",
    "errors": [
        {
            "codes": [
                "Email.employee.email",
                "Email.email",
                "Email.java.lang.String",
                "Email"
            ],
            "arguments": [
                {
                    "codes": [
                        "employee.email",
                        "email"
                    ],
                    "arguments": null,
                    "defaultMessage": "email",
                    "code": "email"
                },
                [],
                {
                    "defaultMessage": ".*",
                    "codes": [
                        ".*"
                    ],
                    "arguments": null
                }
            ],
            "defaultMessage": "must be a well-formed email address",
            "objectName": "employee",
            "field": "email",
            "rejectedValue": "[email protected].",
            "bindingFailure": false,
            "code": "Email"
        },
        {
            "codes": [
                "PositiveOrZero.employee.salary",
                "PositiveOrZero.salary",
                "PositiveOrZero.java.math.BigDecimal",
                "PositiveOrZero"
            ],
            "arguments": [
                {
                    "codes": [
                        "employee.salary",
                        "salary"
                    ],
                    "arguments": null,
                    "defaultMessage": "salary",
                    "code": "salary"
                }
            ],
            "defaultMessage": "must be greater than or equal to 0",
            "objectName": "employee",
            "field": "salary",
            "rejectedValue": -1,
            "bindingFailure": false,
            "code": "PositiveOrZero"
        }
    ],
    "path": "/employee"
}

If we look at the above response it is very verbose. We can customize the response by handling the MethodArgumentNotValidException exception.

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {


    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ErrorDetails handleExceptions(Exception ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(LocalDate.now(),ex.getMessage(), "Exception");
        return errorDetails;
    }

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {

        List<ErrorMessageDto> validationErrorDetails = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> new ErrorMessageDto(error.getObjectName(),error.getField(),error.getDefaultMessage(),error.getRejectedValue().toString()))
                .collect(Collectors.toList());

        ErrorResponse response = new ErrorResponse(status.name(), status.value(),validationErrorDetails);
        return new ResponseEntity<>(response,status );

    }

}

Now the failed validation response looks like below

{
    "error": "BAD_REQUEST",
    "code": 400,
    "errors": [
        {
            "object": "employee",
            "message": "must be a well-formed email address",
            "rejectedValue": "[email protected].",
            "fields": "email"
        },
        {
            "object": "employee",
            "message": "must be greater than or equal to 0",
            "rejectedValue": "-1",
            "fields": "salary"
        }
    ]
}

Validating Path and Query Params

Apart from method parameters, Spring Boot supports validating path variables and query parameters in the URL.

In this scenario, we’re not verifying complex Java objects because path variables and request arguments are simple types like int or their equivalent objects like Integer or String.

Instead of annotating a class field as in the previous example, we add a validation annotation directly to the method argument in the Spring controller

Whenever path variable, query parameter fails validation , framework throws ContraintViolationException

@Validated annotation

To validate path variables, first we need to annotate the controller class with @Validated annotation.

@Validated annotation can be used in the method level but in this scenario we are using at class level.

Next write validations on the path variables like below

@RestController
@RequestMapping("/employee")
@Validated
public class EmployeeController {

    @Autowired
    EmployeeService employeeService;

    @GetMapping()
    public List<Employee> getEmployees() {
        return employeeService.getAllEmployees();
    }


    @GetMapping("/{id}")
    public Employee getEmployee(@PathVariable @Min(1) Integer id) {
        return employeeService.getEmployeeById(id).orElseThrow(() ->new ResponseStatusException(HttpStatus.NOT_FOUND,"Employee not found with id : "+ id));
    }



    @ResponseStatus(HttpStatus.CREATED) // send HTTP 201 instead of 200 as new object created
    @PostMapping   
    public Employee createEmployee(@RequestBody @Valid Employee employee) {       
        return employeeService.createEmployee(employee);
    }

    @PutMapping()
    public Employee updateEmployee(@RequestBody @Valid Employee employee) {
        return employeeService.updateEmployee(employee);
    }

    @DeleteMapping(value="/{id}")
    public  void deleteEmployee(@PathVariable("id") @Min(1) Integer id){
        employeeService.deleteEmployee(id);
    }

    @PatchMapping("/{empId}/dept/{deptId}")
    public Employee updateEmpDepartment(@PathVariable("empId") @Min(1) Integer emp_id , @PathVariable("deptId") @Min(1) Integer dept_id) {
       return employeeService.updateEmpDepartment(emp_id,dept_id);
    }

    @PatchMapping("/{empId}")
    public Employee updateEmpDepartmentById(@PathVariable("empId") Integer emp_id , @RequestBody Department department) {
        return employeeService.updateEmpDepartment(emp_id,department.getId());
    }

    @GetMapping(value="/gender/{gender}")
    public List<Employee> getEmployeesByGender(@PathVariable String gender) {

        return employeeService.findEmployeesByGender(Gender.valueOf(gender));
    }




}

Let’s test the path variable validation

If we look at logs, framework throws ConstraintViolationException since it not handled it throws Internal Server Error to client.

2022-05-09 22:58:17.088 ERROR 24196 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: getEmployee.id: must be greater than or equal to 1] with root cause

javax.validation.ConstraintViolationException: getEmployee.id: must be greater than or equal to 1
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.19.jar:5.3.19]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar:5.3.19]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar:5.3.19]

Instead of sending the InternalServerError to the client, we can customize the exception message by handling the ConstraintViolationException

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

   ....

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public ResponseEntity<Object> handleConstraintViolationException(Exception ex,  WebRequest request) {
       
        ErrorDetails errorDetails = new ErrorDetails(LocalDate.now(),"ConstraintViolationException", ex.getMessage());
        return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
    }



}

Validation Groups to Validate Objects Differently for Different Use Cases

While working with CRUD operations, create and update operations take same object as input but id should be null while creating while updating id should not be null. In complex projects classes are shared between different use cases and those use cases my require different validations. So we need a mechanism to differentiate different use cases.

Bean validations have feature called “Validation Groups” which allows us to invoke different validations for different use cases.

@Validated Annotation

We can use the @Validated annotation to invoke validation groups. This annotation can be used on method and method parameter.

All validation annotations takes groups as one of the parameter where you can specify the group name they belong to.

Note

If we do not specify the group name for validations , they belongs group called Default group. Default group validations are invoked in all usecases.

If we look at the source code for @Null annotations we can see that it will take groups as one of the parameter.

public @interface Null {

	String message() default "{javax.validation.constraints.Null.message}";

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

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

	....
}

First we will create marker interfaces which will represent the validation groups.

public interface OnCreate {

}

public interface OnUpdate {

}

Next we will specify the groups in corresponding validations

@Entity
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Null(groups = OnCreate.class)
    @NotNull(groups = OnUpdate.class)
    private Integer id;

    @NotBlank(message = "FirstName should not be blank")
    @Size(min = 3,message = "FirstName should be at least 3 chars")
    private String first_name;

    @NotBlank(message = "LastName should not be blank")
    @Size(min = 3,message = "LastName should be at least 3 chars")
    private String last_name;

    @Enumerated(EnumType.STRING)
    private Gender gender;

    @PastOrPresent
    private LocalDate birth_date;

    @PastOrPresent
    private LocalDate hire_date;

    @NotBlank // (message = "Email should not be blank")
    @Email    // (message = "Not a valid email")
    private String email;

    @PositiveOrZero
    private BigDecimal salary;

    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;


      .........
}

Next we annotate method with @Validated annotation and specify the groups which needs to be invoked

Using @Validated annotation on method

 @PostMapping
    @Validated(OnCreate.class)
    public Employee createEmployee(@RequestBody @Valid Employee employee) {
        return employeeService.createEmployee(employee);
    }

 @PutMapping()
    @Validated(OnUpdate.class)
    public Employee updateEmployee(@RequestBody @Valid Employee employee) {
        return employeeService.updateEmployee(employee);
    }

Now lets test the validation groups.

First let’s test the employee creation by providing id in the request

First let’s test the employee updating by not providing id in the request

Using @Validated annotation on method parameter

In previous example, we have used @Validated annotation on method. We can also use this annotation directly on the method parameter. When using @Validated on method parameter with group also need to specify the Default group.

 @PutMapping()
    public Employee updateEmployee(@RequestBody  @Validated({OnUpdate.class, Default.class}) Employee employee) {
        return employeeService.updateEmployee(employee);
    }

If we test update function with above change we receive error message like below

What is the difference

When we use the @Validated annotation on the method, Spring Boot first validates the request based on the Default ( i.e validations with out any groups) group validations. If the default group validation passes then it will invoke the group related validations. If Default validation fails, the response will show only validation error messages related to them and it will not invoke the group related validations.

If group validation fails, framework will throw ContraintViolationException.

When we use the @Validated annotation on the method parameter , framework will invoke related group and default group validations and if validation fails it will throws MethodArgumentNotValidException.

Validating Programmatically

Instead of depending on Spring’s built-in Bean Validation functionality, there may be times when you want to invoke validations programmatically. We can achieve that 2 ways.

Using Pre-Configured Validator

Spring Boot provides pre configured validator instance. We can autowire this instance into our controller class and use this instance to invoke validators.

@RestController
@RequestMapping("/department")
public class DepartmentController {

    @Autowired
    DepartmentService departmentService;

    @Autowired
    private Validator validator;
    ...

    @PostMapping
    public Department createDepartment(@RequestBody Department department) {        
        Set<ConstraintViolation<Department>> violations = validator.validate(department);
        if(!violations.isEmpty()) {
            throw  new ConstraintViolationException(violations);
        }
        return departmentService.createDepartment(department);
    }
.....
}

Using Validator API

We can instantiate validator using factory function and use to it to invoke validations.This approach can be used in non spring environments.

@RestController
@RequestMapping("/department")
public class DepartmentController {

    @Autowired
    DepartmentService departmentService;  

    @PutMapping
    public Department updateDepartment(@RequestBody Department department) {
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<Department>> violations = validator.validate(department);
        if(!violations.isEmpty()) {
            throw  new ConstraintViolationException(violations);
        }
        return departmentService.updateDepartment(department);
    }
}

If want to invoke only validations related group you can pass the group name to validate function.

validator.validate(department, OnCreate.class)

Interpolating constraint error messages

Message interpolation is the process of creating error messages for violated Bean Validation constraints. In this section you will learn how such messages are defined and resolved and how you can plug in custom message interpolators in case the default algorithm is not sufficient for your requirements.

Default message interpolation

Spring Boot validation returns default error messages ( gets from hibernate validator framework) when validation fails.

For example

public class Employee implements Serializable {
    ... 
    @NotBlank(message = "FirstName should not be blank")
    @Size(min = 3,message = "FirstName should be at least 3 chars")
    private String first_name;
     ...
    @PastOrPresent
    private LocalDate birth_date;
   ...
}

If validation fails for birthdate – application will show the default in built error message

{
            "object": "employee",
            "message": "must be a date in the past or in the present",
            "rejectedValue": "2023-09-09",
            "fields": "birth_date"
        }

Overriding default error message

Spring Boot allows overriding default error messages by specifying error message with message attribute

If Size validation fails for first_name field- application will show the error message specified with message attribute

{
            "object": "employee",
            "message": "FirstName should be at least 3 chars",
            "rejectedValue": "te",
            "fields": "first_name"
        }

Interpolation with message expressions

it is possible to use the Unified Expression Language (as defined by JSR 341) in constraint violation messages. This allows to define error messages based on conditional logic and also enables advanced formatting options. The validation engine makes the following objects available in the EL context:

  • the attribute values of the constraint mapped to the attribute names
  • the currently validated value (property, bean, method parameter etc.) under the name validatedValue
  • a bean mapped to the name formatter exposing the var-arg method format(String format, Object... args) which behaves like java.util.Formatter.format(String format, Object... args).

The following section provides several examples for using EL expressions in error messages.

Example 1

 @Size(min = 3,message = "FirstName should be at least {min} chars")
    private String first_name;

In above example ${min} resolves to value 3

Example 2

@Size(min = 3, max=10 , message = "FirstName should be between {min}  and {max} chars")
    private String first_name;

In above example ${min} resolves to value 3 and ${max} resolves to value 10.

Example 3

you can also include the value of attribute being validated in the error message with ${validatedValue} expression

@Size(min = 3, max=10 , message = "FirstName - ${validatedValue} should be between {min}  and {max} chars")
    private String first_name;

If bean validation fails for first name , error message will include the first name value in error message

“errors”: [
{
“object”: “employee”,
“message”: “FirstName – XX should be between 3 and 10 chars”,
“rejectedValue”: “XX”,

“fields”: “first_name”
}

Example 4

 @Min(
            value = 2,
            message = "There must be at least {value} seat${value > 1 ? 's' : ''}"
    )
    private int seatCount;

The @Min constraint on seatCount demonstrates how use an EL expression with a ternery expression to dynamically chose singular or plural form, depending on an attribute of the constraint (“There must be at least 1 seat” vs. “There must be at least 2 seats”)

Example 5

 @DecimalMax(
            value = "350",
            message = "The top speed ${formatter.format('%1$.2f', validatedValue)} is higher " +
                    "than {value}"
    )
    private double topSpeed;

the message for the @DecimalMax constraint on topSpeed shows how to format the validated value using the formatter object

Externalizing the error messages

Instead of specifying the error message string directly in the class, you can externalize the error messages.

To externalize the validation error messages

First you need to create file named ValidationMessages.properties in resources folder.

Now put key and error message in the property file

validation.firstNameSize=FirstName should be at least 3 chars

Now refer the error message in the message attribute like blow

@NotBlank(message = "FirstName should not be blank")
    @Size(min = 3, message = "{validation.firstNameSize}")
    
    private String first_name;

Support for i18n

the validation framework supports internationalization of error messages based on Accept-Language header.

Default error messages are supported in various languages.

Foe example, if you send Accept-Language header as fr (French) you will see the error message in french.

If you have custom error messages defined in ValidationMessages.properties file and want to support i18n

you have to create properties file for each locale in below format.

ValidationMessages_XX.properties –> XX should be replaced by locale

The validations discussed in this blog post are in built validations. In cases where these built-in constraints are not sufficient, you can easily create custom constraints tailored to your specific validation requirements. The custom validations can be applied at field or class level where validation depends on multiple fields. I will cover them in next post.

You can download source code from GitHub

You might be also interested in

Similar Posts