Bean Validation

Java Bean Validation is a specification for validating data in Java applications. You can read the current version 2.0 of the specification here. The reference implementation of Bean Validation is Hibernate Validator.

Using Bean Validation brings the following benefits to your project:

  • Validation logic is located next to your domain model: defining value, method, bean constraint is done naturally.

  • Bean Validation standard gives you tens of validation annotations out-of-the-box, like @NotNull, @Size, @Min, @Max, @Pattern, @Email, @Past, less standard like @URL, @Length, and many others.

  • You are not limited by predefined constraints and can define your own constraint annotations. You can also make a new annotation by combining a couple of others, or create a new one and define a Java class that will serve as a validator.

    For example, you can define a class-level annotation @ValidPassportNumber to check that passport number follows the right format, which depends on the location field value.

  • You can put constraints not only on fields and classes but also on methods and method parameters. It is called "validation by contract".

Bean Validation is automatically invoked in UI views when user submits data, and in the generic REST API.

Defining Constraints

You can define constraints using annotations of the jakarta.validation.constraints package or custom annotations. The annotations can be set on an entity or a POJO class declaration, field or getter, and on a service method.

The standard set of constraints includes the most commonly used and universal ones. In addition, Bean Validation allows developers to add their own constraints.

  • @NotNull validates that the annotated property value is not null.

  • @Size validates that the annotated property value has a size between the min and max attributes; can be applied to String, Collection, Map, and array properties.

  • @Min validates that the annotated property has a value higher than or equal to the value attribute.

  • @Max validates that the annotated property has a value less than or equal to the value attribute.

  • @Email validates that the annotated property is a valid email address.

  • @NotEmpty validates that the property is not null or empty; can be applied to String, Collection, Map or Array values.

  • @NotBlank can be applied only to text values and validates that the property is not null or whitespace.

  • @Positive and @PositiveOrZero apply to numeric values and validate that they are strictly positive, or positive, including 0.

  • @Negative and @NegativeOrZero apply to numeric values and validate that they are strictly negative, or negative, including 0.

  • @Past and @PastOrPresent validate that a date value is in the past, or the past including the present.

  • @Future and @FutureOrPresent validate that a date value is in the future, or in the future including the present.

  • @Pattern checks if the annotated string property matches the regular regex expression.

The Studio Entity Designer allows you to quickly set validation annotations to entity attributes.

Entity Validation

Example of using standard validation annotations on entity fields:

Person.java
@JmixEntity
@Table(name = "PERSON", indexes = {
        @Index(name = "IDX_PERSON_LOCATION_ID", columnList = "LOCATION_ID")
})
@Entity
public class Person {
    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private UUID id;

    @InstanceName
    @Length(min = 3) (1)
    @Column(name = "FIRST_NAME", nullable = false)
    @NotNull
    private String firstName;

    @Email(message = "Email address has invalid format: ${validatedValue}",
            regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$") (2)
    @Column(name = "EMAIL", length = 120)
    private String email;

    @DecimalMin(message = "Person height should be positive",
            value = "0", inclusive = false) (3)
    @DecimalMax(message = "Person height can not exceed 300 centimeters",
            value = "300") (4)
    @Column(name = "HEIGHT", precision = 19, scale = 2)
    private BigDecimal height;

    @Column(name = "PASSPORT_NUMBER", nullable = false, length = 15)
    @NotNull
    private String passportNumber;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "LOCATION_ID")
    private Location location;
}
1 Length of a person’s first name should be longer than 3 characters.
2 Email string should be a properly formatted email address.
3 A person’s height should be larger than 0.
4 A person’s height should be less than or equal to 300.

Let’s check how bean validation is running automatically, when user submits data in UI.

validation ui

As you can see, our application not just shows error messages to a user but also highlights form fields that have not passed single-field bean validations with red lines.

Custom Constraints

You can create your domain-specific constraints with programmatic or declarative validation.

To create a constraint with programmatic validation, do the following:

  1. Create an annotation:

    @Target(ElementType.TYPE) (1)
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = ValidPassportNumberValidator.class) (2)
    public @interface ValidPassportNumber {
        String message() default "Passport number is not valid";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    1 Defines that the target of this runtime annotation is a class or an interface.
    2 States that the annotation implementation is in the ValidPassportNumberValidator class.
  2. Create a validator class:

    public class ValidPassportNumberValidator
            implements ConstraintValidator<ValidPassportNumber, Person> {
    
        @Override
        public boolean isValid(Person person, ConstraintValidatorContext context) { (1)
            if (person == null)
                return false;
    
            if (person.getLocation() == null || person.getPassportNumber() == null)
                return false;
    
            return doPassportNumberFormatCheck(person.getLocation(),
                    person.getPassportNumber());
        }
    }
    1 The isValid() method does the actual check.
  3. Use our class-level annotation:

    @ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class})
    @JmixEntity
    @Table(name = "PERSON", indexes = {
            @Index(name = "IDX_PERSON_LOCATION_ID", columnList = "LOCATION_ID")
    })
    @Entity
    public class Person {
    }

You can also create custom constraints using a composition of existing ones, for example:

@NotNull
@Size(min = 2, max = 14)
@Pattern(regexp = "\\d+")
@Target({METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface ValidZipCode {
    String message() default "Zip code is not valid";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

When using a composite constraint, the resulting set of constraint violations will contain separate entries for each enclosed constraint. If you want to return a single violation, annotate the annotation class with @ReportAsSingleViolation.

Validation by Contract

With bean validation, constraints can be applied to the parameters and return values of methods or constructors of any Java type to check for their calls' pre-conditions and post-conditions. This is called "validation by contract".

With the "validation by contract" approach, you have clear, compact, and easily supported code.

Services perform validation of parameters and results if a method has the @Validated annotation in the service interface. For example:

@Validated
public interface PersonApiService {

    String NAME = "sample_PersonApiService";

    @NotNull
    @Valid (1)
    List<Person> getPersons();

    void addNewPerson(@NotNull
                      @Length(min = 3)
                      String firstName,
                      @DecimalMax(message = "Person height can not exceed 300 centimeters",
                              value = "300")
                      @DecimalMin(message = "Person height should be positive",
                              value = "0", inclusive = false)
                      BigDecimal height,
                      @NotNull
                      String passportNumber
    );

    @Validated (2)
    @NotNull
    String validatePerson(@Size(min = 5) String comment,
                          @Valid @NotNull Person person); (3)
}
1 Specifies that every object in the list returned by the getPersons() method needs to be validated against Person class constraints as well.
2 Indicates that the method should be validated.
3 The @Valid annotation can be used if you need the cascaded validation of method parameters. In the example above, the constraints declared on the Person object will be validated as well.

If you perform some custom programmatic validation in a service, use CustomValidationException to inform clients about validation errors in the same format as the standard bean validation does. It can be particularly relevant for REST API clients.

Bean validation is inheritable. If you annotate some class or field or method with a constraint, all descendants that extend or implement this class or interface would be affected by the same constraint check.

Constraint Groups

Constraint groups enable applying only a subset of all defined constraints, depending on the application logic. For example, you may want to force a user to enter a value for an entity attribute, but at the same time to have the ability to set this attribute to null by some internal mechanism. To do it, you should specify the groups attribute on the constraint annotation. Then the constraint will take effect only when the same group is passed to the validation mechanism.

The framework passes the following constraint groups to the validation mechanism:

  • RestApiChecks - bean validation constraint group used by REST API for data validation.

  • UiComponentChecks - bean validation constraint group used by UI for fields validation.

  • UiCrossFieldChecks - bean validation constraint group used by UI for cross-field validation.

  • jakarta.validation.groups.Default - this group is always passed, except saving the entity detail view.

Validation Messages

Constraints can have messages to be displayed to users.

Messages can be set directly in the validation annotations, for example:

@Pattern(message = "Bad formed person last name: ${validatedValue}",
        regexp = "^[A-Z][a-z]*(\\s(([a-z]{1,3})|(([a-z]+\\')?[A-Z][a-z]*)))*$")
@Column(name = "LAST_NAME", nullable = false)
@NotNull
private String lastName;

You can also place the message in the message bundle and specify the message key in the annotation. For example:

@Min(message = "{msg://com.company.demo.entity/Person.age.validation.Min}", value = 14)
@Column(name = "AGE")
private Integer age;

Messages can contain parameters and expressions. Parameters are enclosed in {} and represent either localized messages or annotation parameters, for example {min}, {max}, {value}. Expressions are enclosed in ${} and can include the validated value variable validatedValue, annotation parameters like value or min, and JSR-341 (EL 3.0) expressions. For example:

@Pattern(message = "Invalid name: ${validatedValue}, pattern: {regexp}",
        regexp = "^[A-Z][a-z]*(\\s(([a-z]{1,3})|(([a-z]+\\')?[A-Z][a-z]*)))*$")
@Column(name = "FULL_NAME")
private String fullName;

Localized message values can also contain parameters and expressions.

Running Validation

Validation on Persistence Layer

For JPA entities, bean validation is performed automatically by the EclipseLink ORM framework. It means that when you save an entity instance using DataManager, EntityManager or data repositories, it will be validated and an exception will be thrown if the entity state is invalid.

You can switch the persistence validation off by specifying the following application property:

jakarta.persistence.validation.mode = NONE

Validation in UI

UI components connected to data automatically get BeanPropertyValidator to check the field value. The validator is invoked from the Validatable.validate() method implemented by the visual component and can throw the ValidationException exception.

By default, AbstractBeanValidator has both Default and UiComponentChecks groups.

If an entity attribute is annotated with @NotNull without constraint groups, it will be marked as mandatory in metadata, and UI components binding to data will have required = true.

The datePicker, dateTimePicker and timePicker components automatically set their time range according to the @Past, @PastOrPresent, @Future, @FutureOrPresent annotations.

Entity detail view controllers perform validation against class-level constraints on save if the constraint includes the UiCrossFieldChecks group and if all attribute-level checks are passed.

Validation in REST API

Generic REST API automatically performs bean validation for creating and updating actions and when using the Services API approach.

Programmatic Validation

You can perform bean validation programmatically using the validate() method of jakarta.validation.Validator interface. The result of validation is a set of ConstraintViolation objects. For example:

@Autowired
private Validator validator;

protected void save(Person person) {
    Set<ConstraintViolation<Person>> violations = validator.validate(person);
    // handle collection of violations
}