Tobias Erdle's Blog

Development Engineer working with different technologies. Former Jakarta MVC & Eclipse Krazo Committer. All views are my own.

Github: erdlet | E-Mail: blog (at) erdlet (punkt) de

Modern web applications with Jakarta EE and Tomcat: Part 2 - Setting up Jakarta Bean Validation

This article is the second part of the series "Modern web applications with Jakarta EE and Tomcat" and demonstrates setting up Jakarta Bean Validation and using it in conjunction with Jakarta RESTful Web Services and Jersey.

Topics covered during this article series

Jakarta Bean Validation in a nutshell

Input validation is an essential part of interactive software, as it ensures the consistency of the data. In order to make such validation as simple as possible and universally applicable, the specification Jakarta Bean Validation exists. With the help of the API provided, a validation of the data to be checked can take place at various points both declaratively and programmatically. In addition, Bean Validation, as it is abbreviated in the remainder of this article, supports, among other things, the internationalization of error messages, whereby these can be made accessible to end users.

For example, the annotation @Email can be used to check whether a String matches the expected format of an e-mail address.

import jakarta.validation.constraints.Email;

public class Customer {

    @Email
    private String email;

    //... omitted for brevity
}

More in-depth information on Bean Validation can be found on the specification home page. This article will be focused on how to use Bean Validation in HTTP resources.

Integrating Jakarta Bean Validation

To integrate Bean Validation into a project using Jersey as Jakarta REST implementation, it is necessary to add the dependencies shown below.

  <dependency>
    <groupId>org.glassfish.jersey.inject</groupId>
    <artifactId>jersey-hk2</artifactId>
    <version>3.0.4</version>
  </dependency>

  <dependency>
      <groupId>org.glassfish.jersey.ext</groupId>
      <artifactId>jersey-bean-validation</artifactId>
      <version>3.0.4</version>
  </dependency>

Those new dependencies contain HK2, which is Glassfish's implementation of the Jakarta Inject API for dependency injection, and the Jersey extension for using Bean Validation. The org.glassfish.jersey.ext:jersey-bean-validation dependency contains all necessary dependencies for Bean validation such as the Jakartan Bean Validation API, Hibernate Validator as its reference implementation and other Jakarta APIs like CDI, which will be covered in an following article.

With these two dependencies, it is now possible to declaratively extend the model classes with properties to be validated and to check them on the HTTP interface. The following example is intended to show these steps and also address the topics "Extended error handling" and "Internationalization".

Configuring Jersey

To receive the validation error(s) instead of a generic HTML page as response body, the JakartaRestApplication needs some additional property set.

import org.glassfish.jersey.server.ServerProperties;

@ApplicationPath("")
public class JakartaRestApplication extends Application {

    @Override
    public Map<String, Object> getProperties() {
        return Map.of(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
    }
}

By settings BV_SEND_ERROR_IN_RESPONSE to true, Jersey adds the Bean Validation message to the response body and the client can process it.

Annotate the model

As input data type of the interface a slim DTO (Data Transfer Object) is implemented, which contains the mandatory attributes title and content. These two values are validated on the API side using Bean Validation.

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class PostDTO {

    @Size(min = 10, max = 256)
    private String title;

    @NotBlank
    private String content;

    //... Getter and Setter omitted for brevity
}

The two annotations specify that the title must be between 10 and 256 characters long and that the content must not be empty in principle. Empty in this context means that it must neither be null nor consist only of whitespaces.

Add a POST resource to validate

After there is a model for the data transfer, a corresponding HTTP resource still needs to be implemented. This is mapped to the URI POST /posts and consumes a body with the content type application/x-www-form-urlencoded for the beginning, since thus no further framework for the processing of the content becomes necessary. How to deal with different content types is discussed in the upcoming article on "Content Negotiation".

import jakarta.validation.Valid;

import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;

import jakarta.ws.rs.core.MediaType;

@Path("posts")
public class PostsResource {

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response create(@Valid @BeanParam final PostDTO dto) {

        //... Creating post and location

        return Response.created(location).build();
    }

    //... omitted for brevity
}

As shown in the example, the method uses the annotations @Valid from the Jakarta Bean Validation API and @BeanParam from Jakarta RESTful Web Services. This annotation is used to search for mappings within the class to process the request and simultaneously request validation of the object.

For this to work, the annotation @FormParam must still be added within the DTO with the name of the parameter from the HTTP request. This leads to the following listing.

import jakarta.ws.rs.FormParam;

public class PostDTO {

    @FormParam("title")
    @Size(min = 10, max = 256)
    private String title;

    @FormParam("content")
    @NotBlank
    private String content;

    //... Getter and Setter omitted for brevity
}

Now it can be checked if the POST resource itself works by posting some valid content and expecting a response with HTTP status 201 and a filled location header. In the example, posting the body title=MyFirstTitle&content=MyFirstContent leads to the following, expected response.

POST http://localhost:8080/blogging-app/posts
Content-Type: application/x-www-form-urlencoded

title=MyFirstTitle&content=MyFirstContent


##########################################################

http://localhost:8080/blogging-app/posts

HTTP/1.1 201 
Location: http://localhost:8080/blogging-app/posts/eb5d33f5-37a5-450a-bcca-3d67cecbd17f
Content-Length: 0
Date: Tue, 12 Jul 2022 14:39:04 GMT
Keep-Alive: timeout=20
Connection: keep-alive

Now as the resource itself works, the following chapter is going to discuss how to handle requests which are not valid.

Handle validation errors

To look at the general behavior of Bean Validation in this example, the form parameter title is forgotten in the following request, so the client should get a response with HTTP status 400 and the validation error message inside the body.

POST http://localhost:8080/blogging-app/posts
Content-Type: application/x-www-form-urlencoded

content=MyFirstContent

##########################################################

HTTP/1.1 400 
Vary: Accept
Content-Type: text/plain
Content-Length: 80
Date: Tue, 12 Jul 2022 14:59:39 GMT
Connection: close

must not be blank (path = PostsResource.create.arg0.title, invalidValue = null)

As can be seen from the response, Bean Validation has detected the missing title and returned it as an error to the client. Additionally, the path to the invalid method parameter and the invalid value is provided. Furthermore, once @BeanParam is used, depending on the position in the resource method, the generic value argX exists, where X stands for the position of the

Customize validation messages

If you want to adapt the error message to the client, you can use the resource bundle ValidationMessages.properties. This is located under src/main/resources and is automatically recognized by Bean Validation. You can use the standard messages, like jakarta.validation.constraints.NotBlank.message, as well as your own messages.

In the example, a separate text is to be created for the title as well as for the content. For this purpose the file ValidationMessages.properties is created and the properties PostDTO.title.NotBlank, PostDTO.title.Size and PostDTO.content.NotBlank are added as shown.

PostDTO.title.NotBlank=There must be a non-blank title
PostDTO.title.Size=The title must have between 10 and 256 characters
PostDTO.content.NotBlank=There must be a non-blank content

To display those messages in the response, they have to be set as message property inside the corresponding annotations within PostDTO.

public class PostDTO {

    @FormParam("title")
    @NotBlank(message = "{PostDTO.title.NotBlank}")
    @Size(min = 10, max = 256, message = "{PostDTO.title.Size}")
    private String title;

    @FormParam("content")
    @NotBlank(message = "{PostDTO.content.NotBlank}")
    private String content;

    //... omitted for brevity
}

When the request performed before is sent again, now the custom error message is displayed.

POST http://localhost:8080/blogging-app/posts
Content-Type: application/x-www-form-urlencoded

content=MyFirstContent

##########################################################

http://localhost:8080/blogging-app/posts

HTTP/1.1 400 
Vary: Accept
Content-Type: text/plain
Content-Length: 94
Date: Tue, 12 Jul 2022 15:15:45 GMT
Connection: close

There must be a non-blank title (path = PostsResource.create.arg0.title, invalidValue = null)

Conclusion

Thanks to the Jersey extension, adding Jakarta Bean Validation to the project was straight forward without pitfalls. This helps to add input validation fast and with a well documented API, so different use-cases can be implemented and data consistency ensured.

Upcoming topic: Setting up Jakarta Context and Dependency Injection (CDI)

The next article will cover how to set up Jakarta Context and Dependency Injection and use it in conjunction with Jakarta RESTful Web Services and, especially, Jersey.

Resources

Edits