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 asjava.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.