Getting started with Jakarta Bean Validation

This article covers the Jakarta Bean Validation specification which allows you to express constraints on your model and create custom ones in an extensible way. Within it, we will cover the legacy from Jakarta EE Validation and the new features available since Jakarta EE 10.

Jakarta Bean Validation allows you to write a constraint once and use it in any application layer. Bean Validation is layer agnostic, meaning that you can use the same constraint from the presentation to the business model layer.

jakarta bean validation

Bean Validation allows you to apply a set of built-in constraints in your application, and also write your own and use them to validate beans, attributes, constructors, method returned types, and parameters.

Next, let’s get into details with Built-in Constraints.

Using Built-in Constraints

Constraints are defined by the combination of a constraint annotation and a list of constraint validation implementations. Built-in constraints already have an implementation, therefore you only need to place an annotation (with parameters if any) on your Domain class. For example:

@Min(1)
private int segmentsPerNode;

@Max(1024 * 1024)
@Min(1024)
private long maxBytesPerNode;

The built-in constraints contain in their interface which is the target for your annotation. Typically you can apply it in the following targets:

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})

Therefore, you can also apply the @Email built-in annotations on a method as well:

private String email;

@NotNull @Email
public String getEmail() {
  return email;
}

Finally, the following table contains the list of built-in constraints, available in the jakarta.validation.constraints package, that you can apply in your Model:

ConstraintsDescription
@NullThe element must be null
@NotNullThe element must not be null
@AssertTrueThe element must be true.
@AssertFalseThe element must be false.
@MinThe element must be a number whose value must be higher or equal to the specified minimum.
@MaxThe element must be a number whose value must be lower or equal to the specified maximum.
@DecimalMinThe element must be a number whose value must be higher or equal to the specified minimum
@DecimalMaxThe element must be a number whose value must be lower or equal to the specified minimum
@NegativeThe element must be a strictly negative number (i.e. 0 is considered as an invalid value)
@NegativeOrZeroThe element must be a negative number or zero
@PositiveThe element must be a strictly positive number (i.e. 0 is considered as an invalid value
@PositiveOrZeroThe element must be a positive number or zero
@SizeThe element size must be between the specified boundaries (included).
@DigitsThe element must be a number within accepted range.
@PastThe element must be an instant, date or time in the past.
@PastOrPresentThe element must be an instant, date or time in the past or in the present.
@FutureThe element must be an instant, date or time in the future.
@FutureOrPresentThe element must be an instant, date or time in the present or in the future.
@PatternThe annotated element must match the specified regular expression.
@NotEmptyThe annotated element must not be null nor empty.
@NotBlankThe annotated element must not be null and must contain at least one non-whitespace character.
@EmailThe string has to be a well-formed email address.

Using Group Validation

When you apply validation constraints on a Bean, all the constraints will apply at the same time. What if you need to partially validate your bean or to control the order in which constraints are evaluated ? That’s where Group Validation comes into play!

In order to do that, each constraint must have an element group=Class<?>[] where group has to be an empty interface Let’s see with an example how it works.

public interface GroupUserName {

}

public class User {

        @NotNull(groups = GroupUserName.class)
        String firstName;
        @NotNull(groups = GroupUserName.class)
        String lastName;

        @Size(min = 5, message = "Mail size is at least 5 characters")
        @Pattern(regexp = "[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+", message = "Email format is invalid.")
        private String email;

    // getters and setters. . .

}

In the above example, firstName and lastName will be validated only when you trigger the validate method on the GroupUserName. Here is an example:

User user = new User();
user.setFirstName("John");

Set<ConstraintViolation<User>> constraintViolations =
                       validator.validate(user, GroupUserName.class);

if (constraintViolations.size() > 0) {
      constraintViolations.stream().forEach(
                           ConstraintGroupExample::printError);
} else {
      System.out.println("User is ok");
}

Creating Custom Constraints

A user-defined or custom constraint needs a validation implementation. For example, consider the following Customer class which contains both validation rules on the single fields and a custom validation constraint via the @ValidCustomer annotation:

@RequestScoped
@ValidCustomer(groups = com.itbuzzpress.jsf.validator.CustomerGroup.class)
public class Customer implements Serializable, Cloneable {

    @Pattern(regexp = "[a-z-A-Z]*", message = "First name has invalid characters", groups = com.itbuzzpress.jsf.validator.CustomerGroup.class)
    private String name;

    @Pattern(regexp = "[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+", message = "Please enter a valid formated e-mail !", groups = com.itbuzzpress.jsf.validator.CustomerGroup.class)
    private String email;

    @Min(value = 18, message = "Age must be greater than or equal to 18", groups = com.itbuzzpress.jsf.validator.CustomerGroup.class)
    private int age;

    // . . . .
}

Then, you will need to provide the validation rules in the CustomerValidator class which implements ConstraintValidator:

public class CustomerValidator implements ConstraintValidator<ValidCustomer, Customer> {

    @Override
    public void initialize(ValidCustomer constraintAnnotation) {     }

    @Override
    public boolean isValid(Customer value, ConstraintValidatorContext context) {
            if (value.getEmail().endsWith(".com") && (value.getAge() < 50))  {
                    return true;
              }
            else  {
                    return false;
             }

    }

}

Since the Customer is annotated with the custom annotation @ValidCustomer, we will specify there that the Customer class has a constraints defined in the above CustomerValidator:

@Constraint(validatedBy = {CustomerValidator.class})
@Documented
@Target(TYPE)
@Retention(RUNTIME)
public @interface ValidCustomer {

    String message() default "This customer does not meet our requirements!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Cascade Bean Validation

The Jakarta Bean Validation API does not only allow to validate single class instances but also complete object graphs (cascaded validation). To do so, just annotate a field or property representing a reference to another object with @Valid.

When you apply the @Valid annotation, the validation is recursive. That is, if validated parameter or return value objects have references marked with @Valid themselves, these references will also be validated

See the following example:

public class User {

    @NotNull
    @Valid
    private Person person;

    //...
}

public class Person {

    @NotNull
    private String name;

    //...
}

If you validate an instance of User, a cascade validation will also apply to the referenced Person object. This is because the @Person field is annotated with @Valid. Therefore the @NotNull check will also apply on the Person Class. If the Person class in turns contains a @Valid field, the validation will include also the referenced field.

Finally, please note that since Jakarta validation 2.0 you can also use a more flexible cascaded validation of Collection types. For example you can validate  values and keys of maps as follows:

Map<@Valid CustomerType, @Valid Customer> customersByType

Finally, there is also support for validating container elements by annotating type arguments of parameterized type. Example:

List<@Positive Integer> positiveNumbers 

Building applications using Jakarta Constraints

In order to build applications using Jakarta Bean Validation you need to include the following dependency in your pom.xml file:

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <scope>provided</scope>
</dependency>

Then, in order to run on a Jakarta EE 9/10 container, make sure that all existing javax.validation.* packages are now using jakarta.validation.*.

References: https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html

Found the article helpful? if so please follow us on Socials