Bean Validation
Java Bean Validation is an approach for data validation. The current version is 2.0, described in JSR-380. The reference implementation of Bean Validation is Hibernate Validator.
Using Bean Validation approach brings quite a lot of benefits to your project:
-
Validation logic is located next to your domain model: defining value, method, bean constraint is done naturally that allows to bring the OOP approach to the next level.
-
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 brand 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 thelocation
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 screens when user submits the data, and in the generic REST API.
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 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 notnull
. -
@Size
validates that the annotated property value has a size between themin
andmax
attributes; can be applied toString
,Collection
,Map
, and array properties. -
@Min
validates that the annotated property has a value higher than or equal to thevalue
attribute. -
@Max
validates that the annotated property has a value less than or equal to thevalue
attribute. -
@Email
validates that the annotated property is a valid email address. -
@NotEmpty
validates that the property is notnull
or empty; can be applied toString
,Collection
,Map
orArray
values. -
@NotBlank
can be applied only to text values and validates that the property is notnull
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 regularregex
expression.
Entity Bean Validation
Example of using standard validation annotations on entity fields:
@JmixEntity
@Table(name = "SAMPLE_PERSON", indexes = {
@Index(name = "IDX_PERSON_LOCATION_ID", columnList = "LOCATION_ID")
})
@Entity(name = "sample_Person")
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.
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:
-
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. -
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. -
Use our class-level annotation:
@ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class}) @JmixEntity @Table(name = "SAMPLE_PERSON", indexes = { @Index(name = "IDX_PERSON_LOCATION_ID", columnList = "LOCATION_ID") }) @Entity(name = "sample_Person") 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. -
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(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://datamodel.ex1.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 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.
The standard validator can be removed or initialized with a different constraint group:
@UiController("sample_Person.edit")
@UiDescriptor("person-edit.xml")
@EditedEntityContainer("personDc")
public class PersonEdit extends StandardEditor<Person> {
@Autowired
private TextField<String> passportNumberField;
@Subscribe("removeValidator")
public void onRemoveValidator(Action.ActionPerformedEvent event) {
Collection<? extends Validator<?>> validators =
passportNumberField.getValidators();
for (Validator validator : validators.toArray(new Validator[0])) {
if (validator instanceof BeanPropertyValidator) {
passportNumberField.removeValidator(validator); (1)
}
}
}
@Subscribe("setValidGroups")
public void onSetValidGroups(Action.ActionPerformedEvent event) {
Collection<? extends Validator<?>> validators =
passportNumberField.getValidators();
for (Validator validator : validators.toArray(new Validator[0])) {
if (validator instanceof BeanPropertyValidator) {
((BeanPropertyValidator) validator).setValidationGroups(
new Class[] {UiComponentChecks.class}); (2)
}
}
}
}
1 | Completely removes bean validation from the UI component. |
2 | Here, validators check only the constraints with explicitly set UiComponentChecks group because the Default group will not be passed. |
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 DateField and DatePicker components automatically set their rangeStart
and rangeEnd
properties by the @Past
, @PastOrPresent
, @Future
, @FutureOrPresent
annotations.
Entity editor screens perform validation against class-level constraints on commit if the constraint includes the UiCrossFieldChecks
group and if all attribute-level checks are passed. You can turn off the validation of this kind using the setCrossFieldValidate()
method of the controller:
@UiController("sample_Person.edit")
@UiDescriptor("person-edit.xml")
@EditedEntityContainer("personDc")
public class PersonEdit extends StandardEditor<Person> {
@Subscribe("cancelCrossFValidate")
public void onCancelCrossFValidate(Action.ActionPerformedEvent event) {
setCrossFieldValidate(false);
}
}
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 javax.validation.Validator
interface. The result of validation is a set of ConstraintViolation
objects. For example:
@Autowired
protected Validator validator;
protected void save(Person person) {
Set<ConstraintViolation<Person>> violations = validator.validate(person);
/*
handling of the returned collection of violations
*/
}