Getting Started with Jakarta EE 9: Jakarta Validation
Originally published on 30 Nov 2021
Last updated on 20 Dec 2023
In this last blog of the Getting Started with Jakarta EE 9 blog and video series, we have a look at the Bean Validation specification. Using this specification, you can define some validation rules, from some simple ones on a single field to very complex ones on a business entity, that are reusable depending on the input frameworks you are using within your application.
(See all blogs in this series here)
The checks can be done when data is entered through the JSF screens in the browsers or when data arrives through a REST endpoint. In all situations, the basic principles are the same. We define the data validations checks, the default ones defined in the specification or our custom checks, as annotations on fields and classes.
As mentioned, these validations work in combination with other specifications. We mainly discuss the usage within Jakarta REST and Jakarta REST so you might have a look at the blog entries made for those two specifications.
Validation of Submitted Value to REST Endpoints
Let us get started with some simple validation rules that we apply to parameters we receive on a REST endpoint.
@GET @Produces(MediaType.TEXT_PLAIN) @Path("/sqrt/{value}") public String calculate(@PathParam("value") Double value) { return "SQRT = " + Math.sqrt(value); }
When you receive values from the client, you always need to check the validity of the value. In the above example, the Jakarta REST implementation will already verify f the value on the URL is a numeric value and can be converted to a Double instance. If not, the server will respond with a 404 message since there is no suitable endpoint for the URL found.
But when the user passes a -1 as value, we get a NaN as result. In general we want to validate the data before we try to use them. A simple annotation can perform the check for use, without the need to code any if statements and return a message to the client.
public String calculate(@PathParam("value") @PositiveOrZero Double value)
Using the above method signature, the implementation will check if the supplied value is zero or positive and the code works fine. Other checks that are build in are (not exhaustive list)
@NotBlank
@Size
@Min
@Max
@Past
@Future
@Pattern
Class level Validations
In the previous example, we validated a single parameter or field, but most validation checks are more complex and involve several fields of the instance. These can be coded by the developer using the Custom Validation mechanism.
In the next example we build a check that performs validation on a Customer instance. First, we need to define an annotation that will trigger our custom validation. The definition looks like this
@Retention(RUNTIME) @Target({ TYPE_USE }) @Documented @Constraint(validatedBy = {ValidCustomerCheck.class }) public @interface ValidCustomer { String message() default "Customer is not valid"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; // We can have additional members to configure the check. }
The @Retention
value of RUNTIME is important here since the annotation needs to be kept by the compiler within the bytecode. That way, the Bean Validation implementation can determine what checks need to be performed.
The @Target
list determines where we can use the annotation. In our case, it makes only sense to use the annotation on the class itself, so we have defined the TYPE_USE value.
And the @Constraint
annotation tells the Validation implementation what class must be used to validate instances that are annotated with this annotation.
Each of the annotations that define a custom check, must have the members message, groups, and payload, with the type as defined in the example. The message is obvious I think and a resourcebundle reference can be used to make the message multilingual. The groups member can be used to assign all your checks in 2 or more groups and only perform the checks that are part of one of them. And The payload can be used to add custom data when a validation error happens that can be used by your specific exceptionhandler, for example.
Additional members can be defined that can be used by the validation class in case the validations need to be configurable with some values.
The validation class looks like this
public class ValidCustomerCheck implements ConstraintValidator<ValidCustomer, Customer> { @Override public void initialize(ValidCustomer constraintAnnotation) { // } @Override public boolean isValid(Customer value, ConstraintValidatorContext context) { return value.getTaxId().startsWith(value.getCountryCode()); } }
It must implement the ConstraintValidator
interface so that it can be used without any issues by the Jakarta Validation implementation.
The initialize method takes the annotation instance and can be used to read the additional members that you have defined to make the validation configurable. The isValid method receives an instance of the data that needs to be validated and a context can be used the generate customized messages.
Once the annotation and validation class is in place, you can apply the validation to the Customer instances
@ValidCustomer public class Customer { ... }
And trigger the validation when the Customer class is received in a REST endpoint by defining the @Valid
annotation on the parameter.
Validations in Jakarta Faces
The annotations performing a check on a single field like @NotEmpty
are also picked up during the JSF validation phase of the lifecycle. The custom validations defined on an instance as we did with the @ValidCustomer
annotations are a bit more difficult to achieve.
First of all, we need to define a Marker interface so that we can define a custom group of validations and also link the fields from the JSF view that are involved in the multiple field check. In the demo, I used the ValidCustomerGroup marker interface for this purpose.
public interface ValidCustomerGroup { }
Within the JSF page, we need to indicate the different fields that are involved in the check, for example the country code in the example is defined with the following snippet on the JSF page.
<h:inputText value="#{customerEditBean.customer.countryCode}" label="Country Code" > <f:validateBean validationGroups="jakarta.validation.groups.Default,fish.payara.jakarta.ee9.start.ValidCustomerGroup" /> </h:inputText>
The <f:validateBean>
element indicates that the input value for country code in this case, is part of the multi-field validation indicated by our marker interface. It also has the Default entry as it also has a field validation annotation defined and we want to perform this check also. In our case, we let the Validation implementation first check if the field is not empty before we use it in the comparison with the Tax Id value.
To trigger the multi-field validation itself, we need to define the <f:validateWholeBean> element which indicates the property and thus the validation checks that need to be executed.
<f:validateWholeBean value='#{customerEditBean.customer}' validationGroups="fish.payara.jakarta.ee9.start.ValidCustomerGroup" />
And again, we use here the marker interface so that the implementation can gather all fields required that are involved in the check.
The complete example can be found on Github in this repository and is explained in the video below.
Video - Part A Programmatic Validation
As you can see in the previous section, the multi-field validation in JSF is not obvious to define. Therefore, many developers choose to combine the programmatic triggering of the Bean Validation and integrate this with a custom Exception handler for JSF, or REST, as part of a solution that covers every requirement in the application.
Typically, the service layer is responsible for validating the business requirements before the data is persisted. Typically those services are encoded in a CDI or EJB bean. You can access the Bean Validation process programmatically from these places.
@ApplicationScoped public class CustomerService { @Inject private ValidatorFactory validatorFactory; public void saveCustomer(Customer customer) { Set<ConstraintViolation<Customer>> violations = validatorFactory.getValidator().validate(customer); if (!violations.isEmpty()){ throw new ValidationException(violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList())); } } }
You can inject the ValidatorFactory
from the Validation specification that can be used as a starting point for starting validations in a programmatic way.
From the factory, we can retrieve the default validator that is capable of performing the default and custom-defined checks. The result is a set of violations, errors, that are found. When there are some failing checks, we throw a user-defined Exception, like ValidationException in the snippet. This exception is a RuntimeException and extends from a common business exception. This root exception can be used in an ExceptionHandler for JSF or REST to capture these business exceptions and return the appropriate response.
Validation and JPA
The Validation annotations, the default, and the custom-defined ones can also be used on JPA entity objects. Whenever the Persistence Manager writes data to the database, the cheks are performed and an exception is thrown when data is invalid.
But our opinion is that you should not wait until the data is saved to the database before its validity is checked. That should be done when we receive the data at the REST endpoints or through the JSF pages or at the business layer with the services as we have described in the previous section.
Some of the Validation annotations like @NotEmpty and @Size, are used when the database schema is automatically generated. But also in this case we do not think it is a best practice to do so. The database should be created manually so that all data is properly stored in a way that suits the business. Especially with more complex JPA entities, the generation might not be optimal or clear outside of the scope of the application since other tools like dataware houses might also use them.
Video - Part B
Define Validation Checks on Data Received from Users
With the Jakarta Validation specification, you can define some validation checks on the data that you receive from the users. With the addition of some annotations, the runtime knows what verifications need to be done on a field, parameter or an entire instance with the rules that you have implemented.
These validation checks can be used in combination with Jakarta REST, Faces, and JPA but also in a programmatic way so that they can be incorporated in the business logic verifications done in your service layer.
See all the Getting Started with Jakarta EE 9 videos here.
Read all the Getting Started with Jakarta EE 9 blogs here.
Unlocking the Speed: Performance Tuning for Jakarta EE Applications With JCache
Related Posts
The Payara Monthly Catch - November 2024
Published on 28 Nov 2024
by Chiara Civardi
0 Comments
The Payara Monthly Catch - October 2024
Published on 30 Oct 2024
by Chiara Civardi
0 Comments