Modern web applications with Jakarta EE and Tomcat: Part 2 - Setting up Jakarta Bean Validation
12 Jul 2022 (last modified 26 Jul 2022) - Tobias ErdleThis 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
- Setting up Jakarta REST with Jersey
- Setting up Jakarta Bean Validation (this article)
- Setting up Jakarta Contexts and Dependency Injection
- Setting up Jakarta Persistence
- Setting up Jakarta MVC for server-side rendered frontends
- Content Negotiation with Jakarta REST
- Unit and integration testing
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
- 26 Juli 2022: Reference to database migration tool removed