Java Bean Validation

Java Bean Validation

java bean validation

Overview

Data validation is a crucial process in software development. Invalid data may easily break the application. So, ideally, the input data should be validated as soon as possible and handled in a corresponding way. Since data validation is one of the most common programming logic, it makes sense to find a way to reduce code duplication.

Bean Validation 2.0, defined by JSR 380, is a specification of the Java API for bean validation. It defines a way to validate beans via annotations. There are various annotation constraints available like @Min, @Max, @Size, etc. If default constraints don’t satisfy our needs, custom validators can be implemented.

One of the implementations of Bean Validation is Hibernate Validator.

Maven dependency

In order to use Java Bean Validation in Spring Boot project, the following Maven dependency should be added:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

It includes Hibernate Validator for Bean Validation implementation.

Validation annotations

Let’s create a class with some properties to test validation annotations:

public class Employee {
    
    @NotBlank
    private String id;
    
    @NotBlank
    @Size(min = 1, max = 30)
    private String name;

    @Min(18)
    private int age;
    
    @Email(message = "Email is not valid")
    @NotBlank
    private String email;
    
    @Pattern(regexp = "^[(]\\d{3}[)]\\s\\d{3}-\\d{4}$", message = "The phone number should have the following format:\n (123) 456–8911")
    private String phoneNumber;
    
    @NotBlank
    @Size(min = 6, max = 20)
    private String password;
    
    . . .
}

As you can see, it’s pretty simple to add validation like this. Also, it doesn’t make the code more complicated still being quite readable.

Request parameter validation

The entry point where incoming requests come is usually represented by a controller level. So, it’s a common practice to validate the input data in a controller so that lower levels (facades, services, DAO, etc.) work with already validated data.

So, let’s create a simple controller with a POST method which takes before mentioned Employee object as a request parameter and automatically validates it based on chosen annotations. This tutorial is focused on bean validation details, so any activities with Employee after validation are skipped.

@Controller
public class EmployeeController {
    
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String intro(Model model) {
        model.addAttribute(new Employee());
        
        return "intro";
    }

    @RequestMapping(value = "/employee", method = RequestMethod.POST)
    public String addEmployee(@Valid Employee employee, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "intro";
        }
        return "intro";
    }
    
}

Let’s create a JSP template with an employee form:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<html>
    <head></head>

    <style>
        .error {
            color: red;
            padding-top: 5px;
        }
    </style>

    <body>
    
        <form:form method="POST"
                   action="/employee" modelAttribute="employee">
            <table>
                <tr>
                    <td><form:label path="id">ID</form:label></td>
                    <td><form:input path="id" /></td>
                    <td><form:errors path="id" cssClass="error"/></td>
                </tr>
                <tr>
                    <td><form:label path="name">Name</form:label></td>
                    <td><form:input path="name" /></td>
                    <td><form:errors path="name" cssClass="error"/></td>
                </tr>
                <tr>
                    <td><form:label path="age">Age</form:label></td>
                    <td><form:input  path="age" /></td>
                    <td><form:errors path="age" cssClass="error"/></td>
                </tr>
                <tr>
                    <td><form:label path="email">Email</form:label></td>
                    <td><form:input path="email" /></td>
                    <td><form:errors path="email" cssClass="error"/></td>
                </tr>
                <tr>
                    <td><form:label path="phoneNumber">Phone number</form:label></td>
                    <td><form:input path="phoneNumber" /></td>
                    <td><form:errors path="phoneNumber" cssClass="error"/></td>
                </tr>
                <tr>
                    <td><form:label path="password">Password</form:label></td>
                    <td><form:input path="password" /></td>
                    <td><form:errors path="password" cssClass="error"/></td>
                </tr>
                <tr>
                    <td><input type="submit" value="Submit"/></td>
                </tr>
            </table>
        </form:form>
    </body>
</html>

Let’s fill the following form with invalid data and submit it:

bean validation form

The following result has been received after submitting the form:

bean validation form errors

Request path variable validation

Apart from request parameters, a request path can have parameters to identify resources. So, it would be great to validate them the same way it has been done previously.

@RestController
@RequestMapping("rest")
@Validated
public class EmployeeRestController {

    @PostMapping("/employee")
    public Employee createEmployee(@RequestBody @Valid Employee employee) {
        return employee;
    }
    
    @GetMapping("/employee/{id}")
    public Employee findEmployee(@PathVariable @Min(1) Long id) {
        Employee employee = new Employee();
        employee.setId(String.valueOf(id));
        
        return employee;
    }
}

As you can see, to enable validation, @Validated annotation has been added on a class level and validation constraint annotation
(@Min in this case) on a path variable.

Let’s try to send a GET request to this endpoint via curl:

curl localhost:8080/rest/employee/0

The following result has been received:

{
    "status":"BAD_REQUEST",
    "message":"findEmployee.id: must be greater than or equal to 1",
    "errors":[
        "findEmployee.id: must be greater than or equal to 1"
    ]
}

Custom validation annotations

Available validation annotations cover most of the cases, but still you may have some custom validation logic that repeats throughout the code. In such case, it makes sense to define a custom validation annotations and use it for bean validation.

Let’s add an annotation for phone number validation:

@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhoneNumber {
    
    String message() default "Invalid phone number";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

After that, the validator has to be created:

public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
    
    private static final Pattern PHONE_NUMBER_PATTERN = Pattern.compile("^[(]\\d{3}[)]\\s\\d{3}-\\d{4}$");
    
    @Override
    public boolean isValid(String phoneNumber, ConstraintValidatorContext constraintValidatorContext) {
        return Objects.nonNull(phoneNumber) && PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches();
    }

}

That’s it. The rest of the code stays the same.

Handling validation errors

In case of Spring MVC, validation errors are set into BindingResult object. If the errors are found, the request is usually redirected back to a page with a sent form and validation errors are shown to a user.

In case of REST, the independent client transfers data from/to a server via AJAX requests. So, in case of any errors, the response has to contain a detailed information about what went wrong.

Let’s have a look at how to configure handling of validation errors:

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers,
                                                                  HttpStatus status,
                                                                  WebRequest request) {

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", System.currentTimeMillis());
        body.put("status", status.value());


        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(x -> x.getDefaultMessage())
                .collect(Collectors.toList());

        body.put("errors", errors);

        return new ResponseEntity<>(body, headers, status);

    }

    @ExceptionHandler
    public ResponseEntity<ApiError> constraintViolationException(ConstraintViolationException exception) {
        List<String> errors = new ArrayList<>();
        for (ConstraintViolation<?> violation : exception.getConstraintViolations()) {
            errors.add(violation.getPropertyPath() + ": " + violation.getMessage());
        }

        ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, exception.getLocalizedMessage(), errors);
        
        return new ResponseEntity<>(apiError, new HttpHeaders(), apiError.getStatus());
    }

}
public class ApiError {
    private HttpStatus status;
    private String message;
    private List<String> errors;

    public ApiError(HttpStatus status, String message, List<String> errors) {
        super();
        this.status = status;
        this.message = message;
        this.errors = errors;
    }

    public ApiError(HttpStatus status, String message, String error) {
        super();
        this.status = status;
        this.message = message;
        errors = Arrays.asList(error);
    }

    public HttpStatus getStatus() {
        return status;
    }

    public String getMessage() {
        return message;
    }

    public List<String> getErrors() {
        return errors;
    }
}

If you want to customize validation error messages or translate them into multiple languages, you should define a message source bean and set it for LocalValidatorFactoryBean:

@Configuration
@EnableWebMvc
@ComponentScan("com.keepcodeclean.validation.controller")
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("classpath:messages");
        return messageSource;
    }

    @Bean
    @Primary
    public LocalValidatorFactoryBean getValidator() {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource());
        return bean;
    }
}

After that you can specify message placeholders like this:

@Email(message = "{invalid.email}")

Translations should be put into property files in a resource folder (e.g. messages_en.properties):

invalid.email=The specified email is not valid

Also, the following format can be used:

Pattern.employee.phoneNumber=The phone number should have the following format: (123) 456–8911

where Pattern – annotation name,
employee – class name
phoneNumber – name of a property being validated

Referenecs

The full code is available in a GitHub repo.

Leave a Reply

Your email address will not be published. Required fields are marked *