How To Use The Spring Validator

Carl Mapada
4 min readJan 2, 2022
Validation — source

Introduction

Data validation is an essential part of any web application. This is the process of checking the data to ensure its correctness based on the application requirements. Validation rules are used to verify the validity and correctness of the data. These validation rules can be implemented in the client-side or in the server-side.

In Spring, the de facto standard for validation is the Bean Validation. The API contains standard validators which can be used easily by using simple annotations. Example of the annotations are:

  • @NotNull — validates that the annotated property is not null
  • @NotEmpty — validates that the annotated property is not null or empty
  • @Min — validates that the annotated property has a value no smaller than the value attribute
  • @Max — validates that the annotated property has a value no greater than the value attribute
  • @Email — validates that the annotated property is a valid email address
  • @Past — validates that the date property is in the past
  • @PastOrPresent — validates that the date property is in the past

In most cases, these validator will suffice. However, there are cases where in we need to validate the input data based on some custom business rule. The way to handle this type of situation is to create our own custom validator.

Spring Validator Interface

Spring features a Validator Interface that be used to validate objects. The Validator interface works using an Errors object so that while validating, validators can report validation failures to the Errors object.

Implementing Custom Validator Using Validator Interface

Let say we have a hypothetical FielTrip form that contains the following fields: student name, age, guardian name. Business rule requires the guardian’s name as a required field if the age of the student is less than 18 otherwise this field is optional. Since the guardians name has a dependency on the age of the student, we need to write our custom validator. One way to do this is to use the Validator interface.

public class FieldTripForm{     private String name;
private int age;
private String guardian;
//constructor ...

//getter and setter ...
}

Steps to implement custom validator

  • Create a class that implements the Validator interface.

There are two methods that you need to override when you implement the Validator interface, the support and the validate method. The support method is invoked by Spring prior to the validation. Inside the support method you have to specify the instance of class that the custom validator can validate. This can be done by calling the isAssignableFrom method.

The validate method validates the given object and, in case of validation errors, registers those with the given Errors object. In the example below, we used the rejectIfEmptyOrWhitespace to check if the field is either null or an empty string. In the case of the guardian field, we only perform the validation if age is greater than 18. If any of these form fields is empty, the rejectIfEmptyOrWhitespace method will create a field error and bind it to the field.

@Component
public class FieldTripFormValidator implements Validator{
@Override
public boolean supports(Class clazz) {
return FieldTripForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors){

FieldTripForm form = (FieldTripForm) target;
if(errors.getErrors == 0){
ValidationUtils.rejectIfEmptyOrWhitespace(
errors, "age",
"error.age",
"Age is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(
errors, "name",
"error.name",
"Nameis required.");

if(form.getAge() < 18){
ValidationUtils.rejectIfEmptyOrWhitespace(
errors, "guardian",
"error.guardian",
"Guardian name is required.");
}
}
}
}
  • Add the validator reference in the Controller class
@Autowired
private FieldTripFormValidator ftfValidator;
  • Bind the custom validator to the controller
@InitBinder(value = "fieldTripForm")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(ftfValidator);
}
  • Annotate the controller method parameter with @Valid.

Exception is thrown when validation on an argument annotated with @Valid fails. Specifically, it will throw the MethodArgumentNotValidException.

@RestController
@RequestMapping("/")
public class FieldTripController{
@Autowired
private FieldTripFormValidator ftfValidator;
@Autowired
private FieldTripService ftService;
@InitBinder(value = "fieldTripForm")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(ftfValidator);
}
@PostMapping(value = "/fieldtrip")
public ResponseEntity<APIResponse> signUp(@Valid @RequestBody
FieldTripForm ftf){
APIResponse apiResponse = new APIResponse();
//perform your logic here
return new ResponseEntity<>(apiResponse, HttpStatus.OK); }}
  • Create a GlobalExceptionHandler

It is important to annotate the GlobalExceptionHandler with @ControllerAdvice. This will allow you to handle exception across your application. Inside the GlobalExceptionHandler class, we define a method handleMethodArgumentNotValid which will handle exception of this type.

@ControllerAdvice
public class GlobalExceptionHandler{
@ExceptionHandler({MethodArgumentNotValidException.class}) public final ResponseEntity<Object> handleException(
Exception ex, WebRequest request){
HttpHeaders headers = new HttpHeaders();
if(ex instanceof MethodArgumentNotValidException){
return handleMethodArgumentNotValid(
(MethodArgumentNotValidException) ex, headers,
HttpStatus.BAD_REQUEST, request);
}
return null;
}
private ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status,
WebRequest request){
Map<String, Object> fieldError = new HashMap<>();
List<FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors();
fieldErrors.stream().forEach(error -> fieldError.put(
error.getField(), error.getDefaultMessage()));
Map<String, Object> response= new HashMap<>();
response.put("isSuccess", false);
response.put("data", null);
response.put("status", status);
response.put("fieldError", fieldError);
return new ResponseEntity<>(response, status);
}
}

Testing on Postman

As you can see on the response body of the request, there is a fieldError on the guardian field with a message “Guardian name is required”.

I hope you learn something from this tutorial. Code used in this tutorial can be found on my github. Enjoy and keep on coding.

--

--

Carl Mapada

Senior Full Stack Java Developer with interest on Machine Learning, AI and Data Science. Masters of Science in Computer Science candidate at MIU