DEV Community

Noe Lopez
Noe Lopez

Posted on • Updated on

Spring Rest/MVC Validation

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})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

Let's look at the constraints to be inforced on the DTO:

  1. The name field is required, must have at least 3 charactes and a maximun of 20 characters. It can contain alpha and space characters.
  2. The email field is required and should be a valid email address.
  3. 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) {
}
Enter fullscreen mode Exit fullscreen mode

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)  {
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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)