Introduction
Validating data is a common task that needs to be performed in almost all applications, from the presentation layer to the persistence layer. Java provides a specification (JSR 380) that makes it easy for programmers to validate application constraints. Some of the main features of this specification are:
- The ability to express constraints on object models via annotations.
- The ability to write custom constraints in an extensible way.
- APIs to validate objects and object graphs.
- APIs to validate parameters and return values of methods and constructors.
- reports the set of violations (localized)
- Support for Optional and container elements (generic containers, e.g. List, Map or Optional).
Bean Validation
Bean validation was defined in JSR 380 and its goal is to promote code reusability and decoupling validation logic from other layers of an application. In this tutorial, we will be using Bean Validation 3.0 with anotations. It's worth noting that there is also an XML mapping definition available, for more information visit the offical documentation). The only change in version 3.0 is the package change from javax to jakarta as it is part of Jakarta EE now.
The specification defines a small set of built-in constrains.
Version 1.1 came with : @null, @notnull, @AssertTrue, @AssertFalse, @min, @max, @DecimalMin, @DecimalMax, @Size, @Digits, @Past, @Future and @Pattern.
In version 2.0, new constrains were added: @Email, @NotEmpty, @NotBlank, @positive, @PositiveOrZero, @negative, @NegativeOrZero, @PastOrPresent and @FutureOrPresent.
These annotations can be applied to types, fields, methods, constructors, parameters, container elements or other constraint annotations, in case of composition. This is defined in the annotation class code with the @Target annotation.
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
The specification defines a metadata model and API. The reference implementation for Bean Validation is Hibernate Validator 8, which should not be confused with the Hibernate ORM project.
Adding Validation to Spring Project
Our project is built with Spring boot 3. To include Bean Validation in it, we need to add the below dependency in pom.xml file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
If we explore inside this starter, we can see that the implementation used by Spring is hibernate-validator.
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
<scope>compile</scope>
</dependency>
Once the project depencies are downloaded, we are in a position to start validating our model objects.
Validation in Action
Let's consider adding validation to the customer/document rest api based on previous articles. For example, let's validate the input received when creating a new customer. In this scenario a POST request must be sent.
@PostMapping
public ResponseEntity<Long> addCustomer(
@RequestBody CustomerRequest customerRequest) {
Customer customer = CustomerUtils.convertToCustomer(
customerRequest);
customerRepo.save(customer);
return ResponseEntity.created(URI.create(
CUSTOMER_ENDPOINT_URL+customer.getId()
)).build();
}
The data we want to validate comes in the request body and it is mapped to a CustomerRequest object.
public record CustomerRequest(String name, String email,
LocalDate dateOfBirth) {}
Let's look at the constraints to be inforced on the DTO:
- The name field is required, must have at least 3 charactes and a maximun of 20 characters. It can contain alpha and space characters.
- The email field is required and should be a valid email address.
- The dateOfBirth field is required and must be in the past.
Fortunately, these requirements can be met with built-in constrains provided by Bean Validation:
public record CustomerRequest(
@NotBlank(message = "Name is required.")
@Size(min = 3, max = 20, message = "Name must be at least 3
characters and at most 20 characters.")
@Pattern(regexp = "[a-zA-Z\\s]+", message = "Name can only
contain letters and spaces.") String name,
@NotBlank(message = "Name is required") @Email(message = "Email
is not valid.") String email,
@Past(message = "Date of Birth must be in the past.") LocalDate
dateOfBirth) {
}
In the above code, the validation constraints are defined using annotations. Each constraint is applied to the corresponding field, specifying the validation rules and an error message in case of a validation failure.
There is one more thing to do. To tell Spring to validate the input Object when the method is invoked. Here is where the annotation @Valid is used.
@PostMapping
public ResponseEntity<Long> addCustomer(@Valid @RequestBody
CustomerRequest customerRequest) {
The @Valid annotation is part of JSR-303 and it is applied to the object that requires validation. It ensures that the whole object graph is validated. Nested objects marked with @Valid will be also validated when the parent object is validated. @Valid does not support group validation (more on this on a future article).
Let's run the application and test the endpoint to make it fail. The body of the POST request is described in the following lines
{
"name": "Jo",
"email": "myemail.com",
"dateOfBirth": "2033-04-05"
}
Now Springboot will trigger the constrains set in the CustomerRequest class using the Hibernate Validator implementation. The response sent back from the server is not quite what we expected.
{
"status": 400,
"message": "Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.Long>dev.noelopez.restdemo1.controller.CustomerController.addCustomer(dev.noelopez.restdemo1.dto.CustomerRequest) with 3 errors: [Field error in object 'customerRequest' on field 'name': rejected value [Jo]; codes [Size.customerRequest.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerRequest.name,name]; arguments []; default message [name],20,3]; default message [**Name must be at least 3 characters and at most 20 characters.**]] [Field error in object 'customerRequest' on field 'dateOfBirth': rejected value [2033-04-05]; codes [Past.customerRequest.dateOfBirth,Past.dateOfBirth,Past.java.time.LocalDate,Past]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerRequest.dateOfBirth,dateOfBirth]; arguments []; default message [dateOfBirth]]; default message [**Date of Birth must be in the past.**]] [Field error in object 'customerRequest' on field 'email': rejected value [myemail.com]; codes [Email.customerRequest.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerRequest.email,email]; arguments []; default message [email],[Ljakarta.validation.constraints.Pattern$Flag;@3e7db681,.*]; default message [**Email is not valid.**]] ",
"timestamp": "2023-05-06T07:48:58.2077013"
}
When the validation fails on an argument annotated with @Valid, Spring will throw a MethodArgumentNotValidException. The message in the above JSON is the message coming from this Exception. This is handled by the Controler Advice in our project.
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
RestErrorResponse handleException(Exception ex) {
return new RestErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getLocalizedMessage(),
LocalDateTime.now());
}
What we aim to achieve is presenting the error messages defined in the annotations to the consumer in a clear and readable format. Let's proceed to the next section to understand how to accomplish this.
Handling MethodArgumentNotValidException
As mentioned earlier, the MethodArgumentNotValidException is thrown when any constraints fail during validation. This exception implements the BindingResult interface, which contains the validation results. To address this situation, we need to add a new ExceptionHandler method for handling this specific exception in the Adviser.
1. @ExceptionHandler(MethodArgumentNotValidException.class)
2. @ResponseStatus(HttpStatus.BAD_REQUEST) RestErrorResponse
3. handleException(MethodArgumentNotValidException ex) {
4. String message = ex.getFieldErrors()
5. .stream()
6. .map(e -> " Field "+e.getField() +
7. " Message " + e.getDefaultMessage())
8. .reduce("Errors found:", String::concat);
9. return new RestErrorResponse(HttpStatus.BAD_REQUEST.value(),
message, LocalDateTime.now());
}
Let's take a closer look at the above code snippet. In Lines 1-3 it is declared a handler method to manage any MethodArgumentNotValidException thrown. On line 4, the getFieldErrors method will return the list of FieldErrors. As you can see this method is actually delegating to the bindingResult.
public List<FieldError> getFieldErrors() {
return this.bindingResult.getFieldErrors();
}
On line 6 the map operation receives the FieldError and returns field name and message as String. This is the field where the validation failed and the associated message that was set in the annotations.
On line 8, the terminal reduce operation is called to concatenate each String from the map operation.
Finally, on line 10 the custom error is returned.
Invoking the same POST request sent in the previous section will produce the below response:
{
"status": 400,
"message": "Errors found: Field dateOfBirth. Message Date of Birth must be in the past. Field email. Message Email is not valid. Field name. Message Name must be at least 3 characters and at most 20 characters.",
"timestamp": "2023-05-07T16:09:46.8730112"
}
By implementing this exception handler, the response sent back from the server will provide clear error messages to the consumer, facilitating the identification and resolution of validation errors.
Summary
In this article, we explored JSR 380 and its usage in data validation for Java applications. We saw how to apply validation constraints using annotations, integrate Bean Validation with Spring Boot 3, and handle validation errors in a consumer-friendly manner. The provided code examples and concepts can serve as a foundation for implementing data validation in your own projects.
In future articles, we will delve into topics such as creating custom constraints and performing validations on URLs and request parameters. Stay tuned for more!
As usual code can be found in the project github repo here
Top comments (0)