4.2.13.1. Defining Constraints

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

Example of using standard validation annotations on entity fields:

@Table(name = "DEMO_CUSTOMER")
@Entity(name = "demo$Customer")
public class Customer extends StandardEntity {

    @Size(min = 3) // length of value must be longer then 3 characters
    @Column(name = "NAME", nullable = false)
    protected String name;

    @Min(1) // minimum value
    @Max(5) // maximum value
    @Column(name = "GRADE", nullable = false)
    protected Integer grade;

    @Pattern(regexp = "\\S+@\\S+") // value must conform to the pattern
    @Column(name = "EMAIL")
    protected String email;

    //...
}

Example of using a custom class-level annotation (see below):

@CheckTaskFeasibility(groups = {Default.class, UiCrossFieldChecks.class}) // custom validation annotation
@Table(name = "DEMO_TASK")
@Entity(name = "demo$Task")
public class Task extends StandardEntity {
    //...
}

Example of validation of a service method parameters and return value:

public interface TaskService {
    String NAME = "demo_TaskService";

    @Validated // indicates that the method should be validated
    @NotNull
    String completeTask(@Size(min = 5) String comment, @NotNull Task task);
}
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 an ability to set this attribute to null by some internal mechanism. In order 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 platform passes to the validation mechanism the following constraint groups:

  • RestApiChecks - when validating in REST API.

  • ServiceParametersChecks - when validating service parameters.

  • ServiceResultChecks - when validating service return values.

  • UiComponentChecks - when validating individual UI fields.

  • UiCrossFieldChecks - when validating class-level constraints on entity editor commit.

  • javax.validation.groups.Default - this group is always passed except on the UI editor commit.

Validation Messages

Constraints can have messages to be displayed to users.

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

@Pattern(regexp = "\\S+@\\S+", message = "Invalid format")
@Column(name = "EMAIL")
protected String email;

You can also place the message in a localized messages pack and use the following format to specify the message in an annotation: {msg://message_pack/message_key}. For example:

@Pattern(regexp = "\\S+@\\S+", message = "{msg://com.company.demo.entity/Customer.email.validationMsg}")
@Column(name = "EMAIL")
protected String email;

Messages can contain parameters and expressions. Parameters are enclosed in {} and represent either localized messages or annotation parameters, e.g. {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(regexp = "\\S+@\\S+", message = "Invalid email: ${validatedValue}, pattern: {regexp}")
@Column(name = "EMAIL")
protected String email;

Localized message values can also contain parameters and expressions.

Custom Constraints

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

In order to create a constraint with programmatic validation, do the following:

  1. Create an annotation in the global module of your project and annotate it with @Constraint. The annotation must contain message, groups and payload attributes:

    @Target({ ElementType.TYPE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = TaskFeasibilityValidator.class)
    public @interface CheckTaskFeasibility {
    
        String message() default "{msg://com.company.demo.entity/CheckTaskFeasibility.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
  2. Create a validator class in the global module of your project:

    public class TaskFeasibilityValidator implements ConstraintValidator<CheckTaskFeasibility, Task> {
    
        @Override
        public void initialize(CheckTaskFeasibility constraintAnnotation) {
        }
    
        @Override
        public boolean isValid(Task value, ConstraintValidatorContext context) {
            Date now = AppBeans.get(TimeSource.class).currentTimestamp();
            return !(value.getDueDate().before(DateUtils.addDays(now, 3)) && value.getProgress() < 90);
        }
    }
  3. Use the annotation:

    @CheckTaskFeasibility(groups = UiCrossFieldChecks.class)
    @Table(name = "DEMO_TASK")
    @Entity(name = "demo$Task")
    public class Task extends StandardEntity {
    
        @Future
        @Temporal(TemporalType.DATE)
        @Column(name = "DUE_DATE")
        protected Date dueDate;
    
        @Min(0)
        @Max(100)
        @Column(name = "PROGRESS", nullable = false)
        protected Integer progress;
    
        //...
    }

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 = {})
public @interface ValidProductCode {
    String message() default "{msg://om.company.demo.entity/ValidProductCode.message}";

    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.