Real-World Spring Validation

Brendan Benson

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

Browse the completed version of this tutorial on GitHub: SpringValidationExample

  1. If you already depend on spring-boot-starter-web, then you already have Spring Validation. Otherwise, you’ll have to include org.springframework:spring-context.

  2. 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;
        ...
    }
    

  3. Create a class that implements org.springframework.validation.Validator. (Don’t confuse this with javax.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());
            }
        }
    }
    

  4. Tell Spring to bind the validator to data from web requests. In ProductController, autowire the validator and create a method annotated with @InitBinder.

    private ProductCreateRequestValidator productCreateRequestValidator;
    
    @Autowired
    public ProductController(ProductCreateRequestValidator productCreateRequestValidator) {
        this.productCreateRequestValidator = productCreateRequestValidator;
    }
    
    @InitBinder
    public void setupBinder(WebDataBinder binder) {
        binder.addValidators(productCreateRequestValidator);
    }
    

  5. 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 the ProductCreateRequest.

    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;
        ...
    }
    

  6. 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 the MethodArgumentNotValidException 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;
        ...
    }
    

  7. 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"
            }
        ]
    }
    

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

comments powered by Disqus