Spring Validation enables you to use annotations and interfaces to simplify validation logic. This tutorial provides examples of how to do a few “real-world” validations for a JSON API.
After completing this tutorial, you’ll know how to
- Validate Java objects using custom and built-in validators and annotations
- Handle validation exceptions and present the errors to client-side applications
You’ll be able to write code like:
@Min(0)
private Integer price;
and get JSON responses like:
{
"field": "price",
"code": "Min",
"rejectedValue": -3
}
Browse the completed version of this tutorial on GitHub: SpringValidationExample
If you already depend on
spring-boot-starter-web
, then you already have Spring Validation. Otherwise, you’ll have to includeorg.springframework:spring-context
.Annotate the argument you want validated with the
@Valid
annotation. In this case we’re validating an API request to create a product in our system (in ProductController.java).@RequestMapping(value = "/products", method = RequestMethod.POST) public Product create(@Valid @RequestBody ProductCreateRequest productCreateRequest) { Product product = productCreateRequest.toProduct(); productRepository.save(product); return product; }
class ProductCreateRequest { private String name; private String sku; private Integer price; ... }
public class Product { private String sku; private String name; private Integer price; private LocalDateTime createdAt; ... }
Create a class that implements
org.springframework.validation.Validator
. (Don’t confuse this withjavax.validation.Validator
.)import org.springframework.validation.Validator; @Component public class ProductCreateRequestValidator implements Validator { private ProductRepository productRepository; @Autowired public ProductCreateRequestValidator(ProductRepository productRepository) { this.productRepository = productRepository; } @Override public boolean supports(Class<?> clazz) { return ProductCreateRequest.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { ProductCreateRequest productCreateRequest = (ProductCreateRequest) target; if (productRepository.exists(productCreateRequest.toProduct())) { errors.reject(ALREADY_EXISTS.getCode()); } } }
Tell Spring to bind the validator to data from web requests. In
ProductController
, autowire the validator and create a method annotated with@InitBinder("productCreateRequest")
. (Note: specify the name of the request parameter - otherwise Spring gets confused with multiple binders)private ProductCreateRequestValidator productCreateRequestValidator; @Autowired public ProductController(ProductCreateRequestValidator productCreateRequestValidator) { this.productCreateRequestValidator = productCreateRequestValidator; } @InitBinder("productCreateRequest") public void setupBinder(WebDataBinder binder) { binder.addValidators(productCreateRequestValidator); }
So far, we’ve validated uniqueness of a product by checking if it already exists in our database. To validate that the product has a name and SKU (
@NotBlank
) and a non-negative price (@Min(0)
), we can simply annotate theProductCreateRequest
.import org.hibernate.validator.constraints.NotBlank; import javax.validation.constraints.Min; class ProductCreateRequest { @NotBlank private String name; @NotBlank private String sku; @Min(0) private Integer price; ... }
Unfortunately, if we send invalid data to the
/products
endpoint, Spring will respond with a verbose, unhelpful error message. To have the application to respond with a useful, consistent error message, Spring must intercept theMethodArgumentNotValidException
caused by the invalid data, and format it. Create a@ControllerAdvice
class to handle this exception universally.@ControllerAdvice public class ApiValidationExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request ) { BindingResult bindingResult = ex .getBindingResult(); List<ApiFieldError> apiFieldErrors = bindingResult .getFieldErrors() .stream() .map(fieldError -> new ApiFieldError( fieldError.getField(), fieldError.getCode(), fieldError.getRejectedValue()) ) .collect(toList()); List<ApiGlobalError> apiGlobalErrors = bindingResult .getGlobalErrors() .stream() .map(globalError -> new ApiGlobalError( globalError.getCode()) ) .collect(toList()); ApiErrorsView apiErrorsView = new ApiErrorsView(apiFieldErrors, apiGlobalErrors); return new ResponseEntity<>(apiErrorsView, HttpStatus.UNPROCESSABLE_ENTITY); } }
public class ApiErrorsView { private List<ApiFieldError> fieldErrors; private List<ApiGlobalError> globalErrors; ... }
public class ApiFieldError { private String field; private String code; private Object rejectedValue; ... }
public class ApiGlobalError { private String code; ... }
When the application receives a request with invalid data, it will respond with a status code of 422 (Unprocessable Entity) and a nice JSON description of why.
{ "fieldErrors": [ { "field": "price", "code": "Min", "rejectedValue": -3 } ], "globalErrors": [ { "code": "AlreadyExists" } ] }
Browse the completed code on GitHub: SpringValidationExample
The Spring Validation Documentation explains these concepts further.
How are you doing validation on your Spring project? Let me know in the comments.