Custom Input Validation Using InitBinder- Spring Boot

Validating user input is one of the important tasks while developing any web application. Spring framework provides built-in support for the validation of user input. We can use Spring boot InitBinder to validate the user request.

In the previous article, we learned how to validate the form inputs while developing a Spring MVC web application.

In this article, we will learn how to use Spring boot InitBinder annotation and validate the input JSON request using a custom validator class.

We will create a RESTful POST endpoint and validate the JSON input with the custom validator class.

Technologies used in this article:

  • Spring boot version: 2.2.6.RELEASE
  • Java version 1.8

Create a Spring Boot application

Create a spring boot application with required dependencies.

Add spring-boot-starter-web and Lombok(To reduce boilerplate code) dependencies to the pom.xml of the application.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>

Create DTO classes

Let us create two DTO classes with the names Student and Address.

We will validate the JSON input sent to the REST endpoint and check if the input is valid.

Also, add Getter and Setter Lombok annotations to the DRO classes.

This class contains the namegrade, and boolean address field to identify if address details are available for the student.

The addressDetails field holds the address details.

package com.asbnotebook.dto;
import java.io.Serializable;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Student implements Serializable{
	private static final long serialVersionUID = 1L;
	
	private String name;
	private Integer grade;
	private Boolean address;
	private Address addressDetails;
}

The below code snippet shows the Address DTO class.

package com.asbnotebook.dto;
import java.io.Serializable;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Address implements Serializable {
	private static final long serialVersionUID = 1L;
	private String street;
	private Integer doorNo;
	private String additionalInfo;
}

Custom error response

This is an optional step. We are creating a customized error JSON format by using the custom response error object.

As we are developing a REST endpoint, we can create a custom meaningful JSON response for validation errors.

Create a custom Error response class

Create a java class with the name ApiError and add the below fields.

We have defined a status field, an error field, a count field to list error count, and a list of validation errors that occurred during input validation.

package com.asbnotebook.dto;
import java.io.Serializable;
import java.util.List;
import org.springframework.http.HttpStatus;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ApiError implements Serializable {
	private static final long serialVersionUID = 1L;
	private HttpStatus status;
	private String error;
	private Integer count;
	private List<String> errors;
}

Create a custom error handler class

We need to override the default handler method to use our custom error object.

create a global error handler class with the name ApiErrorHandler.

The class extends the ResponseEntityExceptionHandler class and also overrides the handleMethodArgumentNotValid method and returns a custom error object to the client.

package com.asbnotebook.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import com.asbnotebook.dto.ApiError;
@ControllerAdvice
public class ApiErrorHandler extends ResponseEntityExceptionHandler {
	@Override
	protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
			HttpHeaders headers, HttpStatus status, WebRequest request) {
		ApiError apiError = new ApiError();
		apiError.setCount(ex.getBindingResult().getErrorCount());
		apiError.setStatus(HttpStatus.BAD_REQUEST);
		apiError.setError("Validation failed");
		List<String> errors = new ArrayList<>();
		BindingResult bindingResult = ex.getBindingResult();
		bindingResult.getAllErrors().forEach(error -> errors.add(error.getCode()));
		apiError.setErrors(errors);
		return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST);
	}
}

Create an input validator class

To implement custom validation, we have to create a custom input validator class.

Create a java class with the name StudentValidator.

The class should implement the spring validation API’s Validator interface.

We also need to implement the supports and the validate methods of the Validator interface.

Also, the supports method here checks if the input object is of class type Student.

package com.asbnotebook.validator;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.asbnotebook.dto.Student;
@Component
public class StudentValidator implements Validator {
	@Autowired
	private MessageSource messageSource;
	@Override
	public boolean supports(Class<?> clazz) {
		return Student.class.equals(clazz);
	}
	@Override
	public void validate(Object target, Errors errors) {
		Student student = (Student) target;
		ValidationUtils.rejectIfEmpty(errors, "name",
				messageSource.getMessage("student.name.error", null, Locale.getDefault()));
		if (student.getGrade() <= 0) {
			errors.rejectValue("grade", messageSource.getMessage("student.grade.error", null, Locale.getDefault()),
					"Student grade should be greater than zero");
		}
		if (null != student.getAddress() && Boolean.TRUE.equals(student.getAddress())
				&& null == student.getAddressDetails()) {
			errors.rejectValue("address", messageSource.getMessage("student.address.error", null, Locale.getDefault()),
					"Student address details should not be empty");
		}
	}
}

The validation method handles all the required input validations.

In the above example, we are validating the student name field by using the spring validation API’s ValidationUtils.

We are validating the grade field by checking if its value is greater than zero.

We are also checking the boolean field address. If a student has an address, the field should be true, and the addressDetails field should contain address details.

Add required error messages

In the above custom validator class, we have used spring’s MessageSource to retrieve the validation error messages.

Create a messages.properties file under /src/main/resources folder.

Add the below properties to the created file.

student.name.error= Student Name field can't be empty.
student.grade.error= Grade should be greater than 0.
student.address.error= Student address details should not be empty.

Create the controller layer

Create a REST controller class with the name StudentController.

We have a POST mapping endpoint /student that receives a Student object in the request body.

package com.asbnotebook.controller;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.asbnotebook.dto.Student;
import com.asbnotebook.validator.StudentValidator;
@RestController("/")
public class StudentController {
	@Autowired
	private StudentValidator studentValidator;
	@InitBinder(value = "student")
	void initStudentValidator(WebDataBinder binder) {
		binder.setValidator(studentValidator);
	}
	@PostMapping("/student")
	public ResponseEntity<Student> saveStudent(@RequestBody @Valid Student student) {
		// Other logic here(Calling the service layer,etc.)
		return new ResponseEntity<>(student, HttpStatus.CREATED);
	}
}

The initStudentValidator method is annotated with the @InitBinder annotation. The method registers our custom StudentValidator class to the Webdatabinder as a validator.

The value attribute determines the request parameter, on which the validator should be applied. In our example, the request body parameter with the name student is validated by our custom validator class.

Also, notice the @Valid annotation, which makes sure that the validation is performed on the field.

Using multiple InitBinder methods

The Init Binder is applied to all types of inputs if the value attribute is not mentioned.

We can also use multiple InitBinder methods inside a controller class. For this, we need to specify the particular request type with the value attribute.

@InitBinder(value = "student")
void initStudentValidator(WebDataBinder binder) {
	binder.setValidator(new StudentValidator());
}
@InitBinder(value = "address")
void initAddressValidator(WebDataBinder binder) {
	binder.setValidator(new AddressValidator());
}

Verify the result

Start the spring boot application and also send the invalid data to the POST endpoint.

We get the custom error response.

Init binder for input validation spring boot

Also, if we pass the valid data, the validation passes successfully.

Init binder for input validation spring boot.

Conclusion

In this article, we learned how to use the spring boot InitBinder and the custom validator class to validate the input.

We have also learned how to create custom error messages with the custom error DTO class.

Example code is available on Github.