YAVI (pronounced jɑ-vάɪ) is a lambda based type safe validation for Java.
1. Introduction
YAVI sounds as same as a Japanese slang "YABAI (ヤバイ)" that means awesome or awful depending on the context (like "Crazy"). If you use YAVI, you will surely understand that it means the former.
The concepts are
-
No reflection!
-
No (runtime) annotation!
-
Not only Java Beans!
-
Zero dependency!
If you are not a fan of Bean Validation, YAVI will be an awesome alternative.
YAVI has the following features:
-
Type-safe constraints, unsupported constraints cannot be applied to the wrong type
-
Fluent and intuitive API
-
Constraints on any object. Java Beans, Records, Protocol Buffers, Immutables and anything else.
-
Lots of powerful built-in constraints
-
Easy custom constraints
-
Validation for groups, conditional validation
-
Validation for arguments before creating an object
-
Support for API and combination of validation results and validators that incorporate the concept of functional programming
For the migration from Bean Validation, refer the guide.
2. Getting Started
This content is derived from https://hibernate.org/validator/documentation/getting-started/ |
Welcome to YAVI.
The following paragraphs will guide you through the initial steps required to integrate YAVI into your application.
2.1. Prerequisites
-
Java Runtime >= 8
2.2. Project set up
In order to use YAVI within a Maven project, simply add the following dependency to your pom.xml
:
<dependency>
<groupId>am.ik.yavi</groupId>
<artifactId>yavi</artifactId>
<version>0.9.1</version>
</dependency>
This tutorial uses JUnit 5 and AssertJ. Add the following dependencies as needed:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.21.0</version>
<scope>test</scope>
</dependency>
2.3. Applying constraints
Let’s dive into an example to see how to apply constraints:
Create src/main/java/com/example/Car.java
and write the following code.
package com.example;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;
public class Car {
private final String manufacturer;
private final String licensePlate;
private final int seatCount;
public static final Validator<Car> validator = ValidatorBuilder.<Car>of()
.constraint(Car::getManufacturer, "manufacturer", c -> c.notNull())
.constraint(Car::getLicensePlate, "licensePlate", c -> c.notNull().greaterThanOrEqual(2).lessThanOrEqual(14))
.constraint(Car::getSeatCount, "seatCount", c -> c.greaterThanOrEqual(2))
.build();
public Car(String manufacturer, String licencePlate, int seatCount) {
this.manufacturer = manufacturer;
this.licensePlate = licencePlate;
this.seatCount = seatCount;
}
public String getManufacturer() {
return manufacturer;
}
public String getLicensePlate() {
return licensePlate;
}
public int getSeatCount() {
return seatCount;
}
}
The ValidatorBuilder#constraint
is used to declare the constraints which should be applied to the return values of getter for the Car
instance:
-
manufacturer
must never be null -
licensePlate
must never be null and must be between 2 and 14 characters long -
seatCount
must be at least 2
You can find the complete source code on GitHub. |
2.4. Validating constraints
To perform a validation of these constraints, you use a Validator
instance.
To demonstrate this, let’s have a look at a simple unit test:
Create src/test/java/com/example/CarTest.java
and write the following code.
package com.example;
import am.ik.yavi.core.ConstraintViolations;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class CarTest {
@Test
void manufacturerIsNull() {
final Car car = new Car(null, "DD-AB-123", 4);
final ConstraintViolations violations = Car.validator.validate(car);
assertThat(violations.isValid()).isFalse();
assertThat(violations).hasSize(1);
assertThat(violations.get(0).message()).isEqualTo("\"manufacturer\" must not be null");
}
@Test
void licensePlateTooShort() {
final Car car = new Car("Morris", "D", 4);
final ConstraintViolations violations = Car.validator.validate(car);
assertThat(violations.isValid()).isFalse();
assertThat(violations).hasSize(1);
assertThat(violations.get(0).message()).isEqualTo("The size of \"licensePlate\" must be greater than or equal to 2. The given size is 1");
}
@Test
void seatCountTooLow() {
final Car car = new Car("Morris", "DD-AB-123", 1);
final ConstraintViolations violations = Car.validator.validate(car);
assertThat(violations.isValid()).isFalse();
assertThat(violations).hasSize(1);
assertThat(violations.get(0).message()).isEqualTo("\"seatCount\" must be greater than or equal to 2");
}
@Test
void carIsValid() {
final Car car = new Car("Morris", "DD-AB-123", 2);
final ConstraintViolations violations = Car.validator.validate(car);
assertThat(violations.isValid()).isTrue();
assertThat(violations).hasSize(0);
}
}
Validator
instances are thread-safe and may be reused multiple times.
The validate()
method returns a ConstraintViolations
instance, which you can iterate in order to see which validation errors occurred.
The first three test methods show some expected constraint violations:
-
The
notNull()
constraint onmanufacturer
is violated inmanufacturerIsNull()
-
The
greaterThanOrEqual(int)
constraint onlicensePlate
is violated inlicensePlateTooShort()
-
The
greaterThanOrEqual(int)
constraint onseatCount
is violated inseatCountTooLow()
If the object validates successfully, validate()
returns an empty ConstraintViolations
as you can see in carIsValid()
.
You can also check if the validation was successful with the ConstraintViolations.isValid
method.
3. Using YAVI
This section describes the basic usage of YAVI.
3.1. Defining and obtaining a core Validator
instance
The core validator am.ik.yavi.core.Validator
can be defined and obtained via am.ik.yavi.builder.ValidatorBuilder
.
Here is an example:
Validator<User> validator = ValidatorBuilder.<User> of() // or ValidatorBuilder.of(User.class)
.constraint(User::getName, "name", c -> c.notNull().lessThanOrEqual(20))
.constraint(User::getEmail, "email", c -> c.notNull().greaterThanOrEqual(5).lessThanOrEqual(50).email())
.constraint(User::getAge, "age", c -> c.notNull().greaterThanOrEqual(0).lessThanOrEqual(200))
.build();
ConstraintViolations violations = validator.validate(user);
boolean isValid = violations.isValid();
violations.forEach(x -> System.out.println(x.message()));
YAVI accumulates all violation messages by default. If a violation is found during the validation, it will not be shortcut. If you want to return from the current validation as soon as the first constraint violation occurs, use "Fail fast mode". |
In order to avoid ambiguous type inferences, you can use explicit _<type>
method per type instead of constraint as follows:
Validator<User> validator = ValidatorBuilder.<User> of()
._string(User::getName, "name", c -> c.notNull().lessThanOrEqual(20))
._string(User::getEmail, "email", c -> c.notNull().greaterThanOrEqual(5).lessThanOrEqual(50).email())
._integer(User::getAge, "age", c -> c.notNull().greaterThanOrEqual(0).lessThanOrEqual(200))
.build();
The first argument of the constraint
method does not have to be a "Getter" as long as it is a java.util.function.Function
.
For example, If you want to create a Validator
for Records, you can implement it straightforwardly like bellow:
public record User(String name, String email, int age) {
}
Validator<User> validator = ValidatorBuilder.<User> of()
.constraint(User::name, "name", c -> c.notNull().lessThanOrEqual(20))
.constraint(User::email, "email", c -> c.notNull().greaterThanOrEqual(5).lessThanOrEqual(50).email())
.constraint(User::age, "age", c -> c.notNull().greaterThanOrEqual(0).lessThanOrEqual(200))
.build();
Or if you don’t want your method to expose the target field, you can define the constraint like following:
public class User {
private final String name;
public final Validator<User> validator = ValidatorBuilder.<User> of()
._string(x -> x.name, "name", c -> c.notNull().lessThanOrEqual(20))
.build();
public User(String name) {
this.name = name;
}
// Omits others
}
See "Built-in Constraints" for other constraint rules.
If you don’t like to specify the field name as a string literal, you can use "Annotation Processor" to make it completely type-safe. |
3.2. Constraints on nested objects
You can use nest
method to apply constraints to the nested fields.
You can delegate to the Validator
for the nested field, or you can also define a set of constraints on the nested field inside.
public class Address {
private Country country;
private City city;
// Omits other fields and getters
}
Validator<Country> countryValidator = ValidatorBuilder.<Country> of()
.constraint(Country::getName, "name", c -> c.notBlank().lessThanOrEqual(20))
.build();
Validator<City> cityValidator = ValidatorBuilder.<City> of()
.constraint(City::getName, "name", c -> c.notBlank().lessThanOrEqual(100))
.build();
Validator<Address> validator = ValidatorBuilder.<Address> of()
.nest(Address::getCountry, "country", countryValidator)
.nest(Address::getCity, "city", cityValidator)
.build();
Or:
Validator<Address> validator = ValidatorBuilder.<Address> of()
.nest(Address::getCountry, "country",
b -> b.constraint(Country::getName, "name", c -> c.notBlank().lessThanOrEqual(20)))
.nest(Address::getCity, "city",
b -> b.constraint(City::getName, "name", c -> c.notBlank().lessThanOrEqual(100)))
.build();
If the nested field is nullable, use nestIfPresent
instead of nest
.
3.3. Constraints on elements in a Collection / Map / Array
You can use forEach
method to apply constraints to each element of Collection / Map / Array.
Like nested fields, You can delegate to the Validator
for validating each element,
or you can also define a set of constraints on the elements inside.
public class History {
private final int revision;
public History(int revision) {
this.revision = revision;
}
public int getRevision() {
return revision;
}
}
public class Histories {
private final List<History> value;
public Histories(List<History> value) {
this.value = value;
}
public List<History> asList() {
return value;
}
}
Validator<History> historyValidator = ValidatorBuilder.<History> of()
.constraint(History::getRevision, "revision", c -> c.notNull().greaterThanOrEqual(1))
.build();
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.forEach(Histories::asList, "histories", historyValidator)
.build();
Or:
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.forEach(Histories::asList, "histories",
b -> b.constraint(History::getRevision, "revision", c -> c.notNull().greaterThanOrEqual(1)))
.build();
If the colletion / map / array field is nullable, use forEachIfPresent
instead of forEach
.
For the constraints on elements in a map, only values can be applied out of the box.
If you want to apply constraints on keys in a map, you need to convert the map to key’s
|
3.4. Applying constraints only to specific conditions
You can apply constraints only to specific conditions with am.ik.yavi.core.ConstraintCondition
interface:
Validator<User> validator = ValidatorBuilder.<User> of()
.constraintOnCondition((user, constraintGroup) -> !user.getName().isEmpty(),
b -> b.constraint(User::getEmail, "email", c -> c.email().notEmpty()))
.build();
The constraint above on email will only be activated if the name is not empty.
3.5. Applying constraints only to specific groups
You can apply constraints only to specific groups with am.ik.yavi.core.ConstraintGroup
as a part of ConstraintCondition
as well:
enum Group implements ConstraintGroup {
CREATE, UPDATE, DELETE
}
Validator<User> validator = ValidatorBuilder.<User> of()
.constraintOnCondition(Group.CREATE.toCondition(), b -> b.constraint(User::getId, "id", c -> c.isNull()))
.constraintOnCondition(Group.UPDATE.toCondition(), b -> b.constraint(User::getId, "id", c -> c.notNull()))
.build();
The group to validate can be specified in validate
method:
ConstraintViolations violations = validator.validate(user, Group.CREATE);
You can use a shortcut constraintOnGroup
method
Validator<User> validator = ValidatorBuilder.<User> of()
.constraintOnGroup(Group.CREATE, b -> b.constraint(User::getId, "id", c -> c.isNull()))
.constraintOnGroup(Group.UPDATE, b -> b.constraint(User::getId, "id", c -> c.notNull()))
.build();
Note that all constraints without conditions will be validated for any constraint group.
Also, if no group is specified in the |
3.6. Creating a custom constraint
If you want to apply constraints that are not in the "Built-in Constraints", you can create custom constraints by implementing am.ik.yavi.core.CustomConstraint
interface as bellow:
public class IsbnConstraint implements CustomConstraint<String> {
@Override
public boolean test(String s) {
// Delegate processing to another method
return ISBNValidator.isISBN13(s);
}
@Override
public String messageKey() {
return "string.isbn13";
}
@Override
public String defaultMessageFormat() {
return "\"{0}\" must be ISBN13 format";
}
}
The created custom constraint can be specified by predicate
method as follows:
IsbnConstraint isbn = new IsbnConstraint();
Validator<Book> book = ValidatorBuilder.<Book> of()
.constraint(Book::getTitle, "title", c -> c.notBlank().lessThanOrEqual(64))
.constraint(Book::getIsbn, "isbn", c -> c.notBlank().predicate(isbn))
.build();
You can also write constraint rules directly in the predicate
method instead of defining the CustomConstraint
class.
Validator<Book> book = ValidatorBuilder.<Book> of()
.constraint(Book::getTitle, "title", c -> c.notBlank().lessThanOrEqual(64))
.constraint(Book::getIsbn, "isbn", c -> c.notBlank()
.predicate(s -> ISBNValidator.isISBN13(s), "string.isbn13", "\"{0}\" must be ISBN13 format"))
.build();
The first argument of the violation message is the field name. Also, the last argument is the violated value.
If you want to use other arguments, override arguments
method as bellow:
public class InstantRangeConstraint implements CustomConstraint<Instant> {
private final Instant end;
private final Instant start;
InstantRangeConstraint(Instant start, Instant end) {
this.start = Objects.requireNonNull(start);
this.end = Objects.requireNonNull(end);
}
@Override
public Object[] arguments(Instant violatedValue) {
return new Object[] { this.start /* {1} */, this.end /* {2} */};
}
@Override
public String defaultMessageFormat() {
return "Instant value \"{0}\" must be between \"{1}\" and \"{2}\".";
}
@Override
public String messageKey() {
return "instant.range";
}
@Override
public boolean test(Instant instant) {
return instant.isAfter(this.start) && instant.isBefore(this.end);
}
}
3.7. Cross-field validation
If you want to apply constraints on target class itself, you can use constraintOnTarget
.
It can be used when you want to apply cross-field constraints as follows:
Validator<Range> validator = ValidatorBuilder.<Range> of()
.constraint(range::getFrom, "from", c -> c.greaterThan(0))
.constraint(range::getTo, "to", c -> c.greaterThan(0))
.constraintOnTarget(range -> range.getTo() > range.getFrom(), "to", "to.isGreaterThanFrom", "\"to\" must be greater than \"from\"")
.build();
You can also create a custom constraint for the cross-field validation as follows:
public class RangeConstraint implements CustomConstraint<Range> {
@Override
public String defaultMessageFormat() {
return "\"to\" must be greater than \"from\"";
}
@Override
public String messageKey() {
return "to.isGreaterThanFrom";
}
@Override
public boolean test(Range range) {
return range.getTo() > range.getFrom();
}
}
RangeConstraint range = new RangeConstraint();
Validator<Range> validator = ValidatorBuilder.<Range> of()
.constraintOnTarget(range, "to")
.build();
3.8. Overriding violation messages
The default violation message for each constraint is defined in "Built-in Constraints".
If you want to customize the violation message, append message
method on the target constraint as follows:
Validator<User> validator = ValidatorBuilder.<User> of()
.constraint(User::getName, "name", c -> c.notNull().message("{0} is required!")
.greaterThanOrEqual(1).message("{0} is too small!")
.lessThanOrEqual(20).message("{0} is too large!"))
.build()
3.9. Message Formatter
YAVI provides am.ik.yavi.message.MessageFormatter
interface for constructing violation messages.
By default, am.ik.yavi.message.SimpleMessageFormatter
is used, which simply uses java.text.MessageFormatter
to interpolate the message.
A list of message keys and default message formats is given in "Built-in Constraints".
As a feature of error messages, the following are supported compared to Bean Validation:
The first placeholder Especially for the second one, since it is not supported by the general Validation library, for example, even if the error message "xyz should be 100 characters or less" is returned, what characters are actually entered now? Sometimes I try to cut the letters little by little because I don’t know if they are counted. By default, the following message is displayed so that the user does not have to do this wasteful thing. |
If you want to customize the message interpolation, implement MessageFormatter
.
As an example, the implementation that reads messages in messages.properties
is shown as follows:
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import am.ik.yavi.message.MessageFormatter;
public enum ResourceBundleMessageFormatter implements MessageFormatter {
SINGLETON;
@Override
public String format(String messageKey, String defaultMessageFormat, Object[] args,
Locale locale) {
ResourceBundle resourceBundle = ResourceBundle.getBundle("messages", locale);
String format;
try {
format = resourceBundle.getString(messageKey);
}
catch (MissingResourceException e) {
format = defaultMessageFormat;
}
try {
String target = resourceBundle.getString((String) args[0] /* field name */);
args[0] = target;
}
catch (MissingResourceException e) {
}
return new MessageFormat(format, locale).format(args);
}
}
If you want to replace the MessageFormatter
, you can set it as follows.
Validator<User> validator = ValidatorBuilder.<User> of()
.messageFormatter(ResourceBundleMessageFormatter.SINGLETON)
// ...
.build();
3.10. Fail fast mode
Using the fail fast mode, YAVI allows to return from the current validation as soon as the first constraint violation occurs. This can be useful for the validation of large object graphs where you are only interested in a quick check whether there is any constraint violation at all.
Validator<User> validator = ValidatorBuilder.<User> of()
.constraint(User::getName, "name", c -> c.notNull().lessThanOrEqual(20))
.constraint(User::getEmail, "email", c -> c.notNull().greaterThanOrEqual(5).lessThanOrEqual(50).email())
.constraint(User::getAge, "age", c -> c.notNull().greaterThanOrEqual(0).lessThanOrEqual(200))
.failFast(true) // <-- Enable the fail fast mode
.build();
You can switch an existing Validator
to the fail fast mode as follows:
Validator<User> failFastValidator = validator.failFast(true);
This feature comes from the fail fast mode of Hibernate Validator. |
3.11. Kotlin Support
If you are using Kotlin, you can define a Validator
with DSL as follows:
val validator = validator<User> {
User::name {
notNull()
lessThanOrEqual(20)
}
User::email {
notNull()
greaterThanOrEqual(5)
lessThanOrEqual(50)
email()
}
User::age {
notNull()
greaterThanOrEqual(0)
lessThanOrEqual(100)
}
}
Field names can be overridden like bellow:
val validator = validator<User> {
(User::name)("Name") {
notNull()
lessThanOrEqual(20)
}
(User::email)("Email") {
notNull()
greaterThanOrEqual(5)
lessThanOrEqual(50)
email()
}
(User::age)("Age") {
notNull()
greaterThanOrEqual(0)
lessThanOrEqual(100)
}
}
forEach
example:
val validator = validator<DemoForEachCollection> {
DemoForEachCollection::x forEach {
DemoString::x {
greaterThan(1)
lessThan(5)
}
}
}
nest
example:
val validator = validator<DemoNested> {
DemoNested::x nest {
DemoString::x {
greaterThan(1)
lessThan(5)
}
}
}
nest
delegate example:
val validator = validator<DemoNested> {
DemoNested::x nest demoStringValidator
}
4. Combining validation results
YAVI supports a functional programming concept known as Applicative Functor.
A sequence of validations are executed while accumulating the results (ConstraintViolation
), even if some or all of these validations fail during the execution chain.
It is helpful when you want to combine validation results of multiple Value Objects to produce a new object. (Of course, it is also useful for any objects other than Value Objects.)
4.1. Validating with Applicative Functor
am.ik.yavi.fn.Validation<E, T>
class is the implementation of Applicative Functor. E
is the type of error and T
is the type of target object.
It can be obtained by am.ik.yavi.core.ApplicativeValidator
that can be converted from Validator
by applicative()
method.
Validator<User> validator = ValidatorBuilder.<User> of()
.constraint(User::getName, "name", c -> c.notNull().lessThanOrEqual(20))
// ...
.build();
ApplicativeValidator<User> applicativeValidator = validator.applicative();
am.ik.yavi.core.Validated<T>
is a shortcut of Validation<ConstraintViolation, T>
which is specialized for Validator
's usage.
Validator<Email> emailValidator = ValidatorBuilder.<Email> of()
.constraint(Email::value, "email", c -> c.notBlank().email())
.build();
Validator<PhoneNumber> phoneNumberValidator = ValidatorBuilder.<PhoneNumber> of()
.constraint(PhoneNumber::value, "phoneNumber", c -> c.notBlank().pattern("[0-9\\-]+"))
.build();
Validated<Email> emailValidated = emailValidator.applicative().validate(email);
Validated<PhoneNumber> phoneNumberValidated = phoneNumberValidator.applicative().validate(phoneNumber);
The validated target or constraint violations can be retrieved from the Validated
instance as follows:
if (emailValidated.isValid()) {
Email email = emailValidated.value(); // throws NoSuchElementException if it is invalid
} else {
ConstraintViolations violations = emailValidated.errors(); // throws NoSuchElementException if it is valid
}
// or
Email email = emailValidated.orElseThrow(violations -> new ConstraintViolationsException(violations));
fold
method is convenient if you want to create an instance of the same type regardless of the success or failure of the validation.
HttpStatus status = emailValidated.fold(violations -> HttpStatus.BAD_REQUEST, email -> HttpStatus.OK);
4.2. Combining Validation
/ Validated
objects
Validation
/ Validated
objects can be combined to produce a new object.
In the bellow example, ContactInfo
instance is created using Email
and PhoneNumber
after validating them.
Validated<ContactInfo> contactInfoValidated = emailValidated.combine(phoneNumberValidated)
.apply((em, ph) -> new ContactInfo(em, ph));
// or
Validated<ContactInfo> contactInfoValidated = Validations.combine(emailValidated, phoneNumberValidated)
.apply((em, ph) -> new ContactInfo(em, ph));
The important thing here is that even if the validation of Email
or PhoneNumber
fails, all validation results are accumulated without shortcuts in the middle.
For example, if you put a blank space in Email
and a
in PhoneNumber
and try to create a ContactInfo
, the validation will fail, and you will get the following three ContraintViolation
s:
* "email" must not be blank * "email" must be a valid email address * "phoneNumber" must match [0-9\-]+
Validation for
Alternatively, you can combine
|
5. Validating arguments
YAVI supports validating arguments of a constructor or factory method before creating an object with Arguments Validator.
This is one of YAVI’s unique features.
5.1. Defining and obtaining an Arguments Validator instance
Arguments Validator, as the name implies, is a Validator for arguments.
A normal Validator, like Bean Validation, validates if the values are valid for the object to which the values are set, while the Arguments Validator validates that the arguments set to the Object are valid.
Since an object is created before validation in a normal Validator, there is a possibility of creating an object in an incomplete state temporarily.
For example, even if the constraint of notNull()
is imposed on the Validator side,
if the null
check is implemented in the constructor, this null
check will work before the validation by Validator is executed.
The Arguments Validator creates an object after validation, so you don’t have to worry about an incomplete object.
public class Person {
public Person(String name, String email, Integer age) {
// ...
}
public static Arguments3Validator<String, String, Integer, Person> validator = ArgumentsValidatorBuilder
.of(Person::new)
.builder(b -> b
._string(Arguments1::arg1, "name", c -> c.notBlank().lessThanOrEqual(100)) // Constrains onf the first argument of Person::new
._string(Arguments2::arg2, "email", c -> c.notBlank().lessThanOrEqual(100).email()) // Constrains onf the second argument of Person::new
._integer(Arguments3::arg3, "age", c -> c.greaterThanOrEqual(0).lessThan(200))) // Constrains onf the third argument of Person::new
.build();
}
The Arguments Validator can be used as follows:
Validated<Person> personValidated = Person.validator.validate("Jone Doe", "jdoe@example.com", 30);
am.ik.yavi.arguments.Arguments1Validator
to am.ik.yavi.arguments.Arguments16Validator
are available.
Arguments Validator is useful, but it can be a bit verbose to define. Since it is Type-Safe, it is not possible to define something that does not fit the type, but it feels like a puzzle that fits the type. Building the Arguments Validator can be simplified by "Defining a Validator for a single value" and "Combining validators".
5.2. Validating method arguments
Arguments Validator can be used for validating method arguments as well.
// From https://beanvalidation.org/
public class UserService {
public User createUser(/* @Email */ String email,
/* @NotNull */ String name) {
// ...
}
}
The arguments for UserService.createUser
in the above example can be validated as follows:
Arguments3Validator<UserService, String, String, User> validator = ArgumentsValidatorBuilder
.of(UserService::createUser)
.builder(b -> b
._object(Arguments1::arg1, "userService", c -> c.notNull())
._string(Arguments2::arg2, "email", c -> c.email())
._string(Arguments3::arg3, "name", c -> c.notNull()))
.build();
UserService userService = new UserService();
Validated<User> userValidated = validator.validate(userService, "jdoe@example.com", "John Doe");
void cannot be used as return type while java.lang.Void is available.
|
5.3. Defining a Validator for a single value
If you just want to define Arguments1Validator
for a single String
or Integer
etc, you can write:
StringValidator<String> nameValidator = StringValidatorBuilder
.of("name", c -> c.notBlank().lessThanOrEqual(100))
.build(); // -> extends Arguments1Validator<String, String>
StringValidator<String> emailValidator = StringValidatorBuilder
.of("email", c -> c.notBlank().lessThanOrEqual(100).email())
.build(); // -> extends Arguments1Validator<String, String>
IntegerValidator<Integer> ageValidator = IntegerValidatorBuilder
.of("age", c -> c.greaterThanOrEqual(0).lessThan(200))
.build(); // -> extends Arguments1Validator<Integer, Integer>
Validated<String> nameValidated = nameValidator.validate("Jone Doe");
Validated<String> emailValidated = nameValidator.validate("jdoe@example.com");
Validated<Integer> ageValidated = nameValidator.validate(30);
You can convert it to a Value Object after validation by using the andThen
method as follows:
StringValidator<Name> nameValidator = StringValidatorBuilder
.of("name", c -> c.notBlank().lessThanOrEqual(100))
.build()
.andThen(name -> new Name(name)); // -> extends Arguments1Validator<String, Name>
StringValidator<Email> emailValidator = StringValidatorBuilder
.of("email", c -> c.notBlank().lessThanOrEqual(100).email())
.build()
.andThen(email -> new Email(email)); // -> extends Arguments1Validator<String, Email>
IntegerValidator<Age> ageValidator = IntegerValidatorBuilder
.of("age", c -> c.greaterThanOrEqual(0).lessThan(200))
.build()
.andThen(age -> new Age(age)); // -> extends Arguments1Validator<Integer, Age>
Validated<Name> nameValidated = nameValidator.validate("Jone Doe");
Validated<Email> emailValidated = nameValidator.validate("jdoe@example.com");
Validated<Age> ageValidated = nameValidator.validate(30);
If you want to create an Arguments1Validator
for a List
, you can "lift" a Validator
for the element of the list as follows:
Arguments1Validator<Iterable<String>, List<Email>> emailsValidator = ArgumentsValidators.liftList(emailValidator);
Validated<List<Email>> emailsValidated = emailsValidator.validate(List.of("foo@example.com", "bar@example.com"));
// or
Validated<List<Email>> emailsValidated = emailValidator.liftList().validate(List.of("foo@example.com", "bar@example.com"));
liftSet()
, listCollection(Supplier)
and liftOptional()
are available as well.
These "small Validators" can be very powerful parts by using the Validator combinators described in "Combining validators".
6. Combining validators
ApplicativeValidator<T>
and Arguments1Validator<S, T>
we’ve seen so far extend ValueValidator<S, T>
.
(T
is the type of the target object to be validated and S
is the type of the source object to create the target object.)
ApplicativeValidator<T>
is a ValueValidator<T, T>
.
Multiple ValueValidator
s can be combined to validate a larger object.
6.1. Splitting a validator for an object into small pieces
You can use split
method to split a validator for the arguments of a constructor or a factory method for creating a large object into small validators and combine them.
Let’s take a look at an example.
Validator for constructor Person(String, String, Integer)
is Arguments3Validator<String, String, Integer, Person>
.
This can be divided into two StringValidator<String>
s and one IntegerValidator<Integer>
and then combined.
It can be defined as bellow:
public class Person {
public Person(String name, String email, Integer age) {
// ...
}
}
StringValidator<String> nameValidator = /* see examples above */;
StringValidator<String> emailValidator = /* see examples above */;
IntegerValidator<Integer> ageValidator = /* see examples above */;
Arguments3Validator<String, String, Integer, Person> personValidator = ArgumentsValidators
.split(nameValidator, emailValidator, ageValidator)
.apply(Person::new);
// or
Arguments3Validator<String, String, Integer, Person> personValidator = nameValidator
.split(emailValidator)
.split(ageValidator)
.apply(Person::new);
The same goes for Validators for Value Objects.
public class Person {
public Person(Name name, Email email, Age age) {
// ...
}
}
StringValidator<Name> nameValidator = /* see examples above */;
StringValidator<Email> emailValidator = /* see examples above */;
IntegerValidator<Age> ageValidator = /* see examples above */;
Arguments3Validator<String, String, Integer, Person> personValidator = ArgumentsValidators
.split(nameValidator, emailValidator, ageValidator)
.apply(Person::new);
// or
Arguments3Validator<String, String, Integer, Person> personValidator = nameValidator
.split(emailValidator)
.split(ageValidator)
.apply(Person::new);
The created Validator can be used as follows:
Validated<Person> personValidated = Person.validator.validate("Jone Doe", "jdoe@example.com", 30);
Since the arguments of ArgumentsValidators#split
are ValueValidator
s rather than Arguments1Validator
s, you can also combine ApplicativeValidator
s as follows:
ApplicativeValidator<Email> emailValidator = /* see examples above */;
ApplicativeValidator<PhoneNumber> phoneNumberValidator = /* see examples above */;
Arguments2Validator<Email, PhoneNumber, ContactInfo> contactInfoValidator = ArgumentsValidators
.split(emailValidator, phoneNumberValidator)
.apply(ContactInfo::new);
Validated<ContactInfo> contactInfoValidated = contactInfoValidator.validate(new Email("yavi@example.com"), new PhoneNumber("090-123-4567"));
6.2. Validating the source object before creating the target object
In a web application, you often get values from a Map
, Form Object, or HttpServletRequest
to create a Domain Object.
Arguments1Validator
can validate the source object and then convert it to the target object using compose
method.
For example, in the case where name
, email
and age
are obtained from HttpServletRequest
object as HTTP request parameters in Servlet, it can be defined as follows:
Argument1Validator<HttpServletRequest, Name> requestNameValidator = nameValidator
.compose(req -> req.getParameter("name"));
Argument1Validator<HttpServletRequest, Email> requestEmailValidator = emailValidator
.compose(req -> req.getParameter("email"));
Argument1Validator<HttpServletRequest, Age> requestAgeValidator = ageValidator
.compose(req -> Integer.valueOf(req.getParameter("age")));
HttpServletRequest request = ...;
Validated<Name> nameValidated = requestNameValidator.validate(request);
Validated<Email> emailValidated = requestEmailValidator.validate(request);
Validated<Age> ageValidated = requestAgeValidator.validate(request);
You can combine these Validators with combine
method as follows to create a Validator for Person
object:
Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = ArgumentsValidators
.combine(requestNameValidator, requestEmailValidator, requestAgeValidator)
.apply(Person::new);
// or
Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = requestNameValidator
.combine(requestEmailValidator)
.combine(requestAgeValidator)
.apply(Person::new);
HttpServletRequest request = ...;
Validated<Person> personValidated = requestPersonValidator.validate(request);
This Validator can also be converted from Arguments3Validator<String, String, Integer, Person>
as follows:
Arguments3Validator<String, String, Integer, Person> personValidator = /* see examples above */;
Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = personValidator
.compose(req -> Arguments.of(req.getParameter("name"), req.getParameter("email"), Integer.valueOf(req.getParameter("age"))));
By combining Validators in this way, you can create various patterns of Validators. Creating a small Arguments Validator not only allows you to validate the values before creating the object, but also makes it more reusable.
7. Built-in Constraints
YAVI includes many commonly used built-in constraints for various types.
7.1. String
Apply the constraints on the String
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
.constraint(/* Function from TargetClass to String */, /* Field Name */, /* Constraints */)
.build();
To avoid ambiguous type inferences, you can use explicit _string
instead of constraint
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
._string(/* Function from TargetClass to String */, /* Field Name */, /* Constraints */)
.build();
If you want to apply constraints to the more generic CharSequence
, use _charSequence
method.
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
._charSequence(/* Function from TargetClass to CharSequence */, /* Field Name */, /* Constraints */)
.build();
7.1.1. notNull()
Checks that the target value is not null
Validator<Email> validator = ValidatorBuilder.<Email> of()
.constraint(Email::asString, "email", c -> c.notNull())
.build();
ConstraintViolations violations = validator.validate(new Email("yavi@example.com")); // Valid
ConstraintViolations violations = validator.validate(new Email(null)); // Invalid ("email" must not be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.2. isNull()
Checks that the target value is null
Validator<User> validator = ValidatorBuilder.<User> of()
.constraint(User::getId, "id", c -> c.isNull())
.build();
ConstraintViolations violations = validator.validate(new User(null)); // Valid
ConstraintViolations violations = validator.validate(new User("1234")); // Invalid ("id" must be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.3. notEmpty()
Checks whether the target value is not null
nor empty
Validator<Email> validator = ValidatorBuilder.<Email> of()
.constraint(Email::asString, "email", c -> c.notEmpty())
.build();
ConstraintViolations violations = validator.validate(new Email("yavi@example.com")); // Valid
ConstraintViolations violations = validator.validate(new Email(null)); // Invalid ("email" must not be empty)
ConstraintViolations violations = validator.validate(new Email("")); // Invalid ("email" must not be empty)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.4. notBlank()
Checks that the target value is not null
, and the trimmed userId is greater than 0
.
The difference to notEmpty()
is that trailing white-spaces are ignored.
Validator<Email> validator = ValidatorBuilder.<Email> of()
.constraint(Email::asString, "email", c -> c.notBlank())
.build();
ConstraintViolations violations = validator.validate(new Email("yavi@example.com")); // Valid
ConstraintViolations violations = validator.validate(new Email(null)); // Invalid ("email" must not be blank)
ConstraintViolations violations = validator.validate(new Email(" ")); // Invalid ("email" must not be blank)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.5. fixedSize(int)
Checks if the target value’s size is the specified size
Validator<ZipCode> validator = ValidatorBuilder.<ZipCode> of()
.constraint(ZipCode::asString, "zipCode", c -> c.fixedSize(7))
.build();
ConstraintViolations violations = validator.validate(new ZipCode("1234567")); // Valid
ConstraintViolations violations = validator.validate(new ZipCode(null)); // Valid
ConstraintViolations violations = validator.validate(new ZipCode("123-4567")); // Invalid (The size of "zipCode" must be 7. The given size is 8)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.6. greaterThan(int)
Checks if the target value’s size is greater than the specified size
Validator<Country> validator = ValidatorBuilder.<Country> of()
.constraint(Country::asString, "country", c -> c.greaterThan(2))
.build();
ConstraintViolations violations = validator.validate(new Country("Japan")); // Valid
ConstraintViolations violations = validator.validate(new Country(null)); // Valid
ConstraintViolations violations = validator.validate(new Country("J")); // Invalid (The size of "country" must be greater than 2. The given size is 1)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.7. greaterThanOrEqual(int)
Checks if the target value’s size is greater than or equals to the specified size
Validator<Country> validator = ValidatorBuilder.<Country> of()
.constraint(Country::asString, "country", c -> c.greaterThanOrEqual(2))
.build();
ConstraintViolations violations = validator.validate(new Country("Japan")); // Valid
ConstraintViolations violations = validator.validate(new Country(null)); // Valid
ConstraintViolations violations = validator.validate(new Country("J")); // Invalid (The size of "country" must be greater than or equal to 2. The given size is 1)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.8. lessThan(int)
Checks if the target value’s size is less than the specified size
Validator<Country> validator = ValidatorBuilder.<Country> of()
.constraint(Country::asString, "country", c -> c.lessThan(4))
.build();
ConstraintViolations violations = validator.validate(new Country("JP")); // Valid
ConstraintViolations violations = validator.validate(new Country(null)); // Valid
ConstraintViolations violations = validator.validate(new Country("Japan")); // Invalid (The size of "country" must be less than 4. The given size is 5)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.9. lessThanOrEqual(int)
Checks if the target value’s size is less than or equals to the specified size
Validator<Country> validator = ValidatorBuilder.<Country> of()
.constraint(Country::asString, "country", c -> c.lessThanOrEqual(4))
.build();
ConstraintViolations violations = validator.validate(new Country("JP")); // Valid
ConstraintViolations violations = validator.validate(new Country(null)); // Valid
ConstraintViolations violations = validator.validate(new Country("Japan")); // Invalid (The size of "country" must be less than or equal to to 4. The given size is 5)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.10. contains(CharSequence)
Checks if the target value contains the specified sequence of char values
Validator<ZipCode> validator = ValidatorBuilder.<ZipCode> of()
.constraint(ZipCode::asString, "zipCode", c -> c.contains("-"))
.build();
ConstraintViolations violations = validator.validate(new ZipCode("123-4567")); // Valid
ConstraintViolations violations = validator.validate(new ZipCode(null)); // Valid
ConstraintViolations violations = validator.validate(new ZipCode("1234567")); // Invalid ("zipCode" must contain -)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.11. pattern(String)
Checks if the target value matches the specified regular expression
Validator<ZipCode> validator = ValidatorBuilder.<ZipCode> of()
.constraint(ZipCode::asString, "zipCode", c -> c.pattern("[0-9]{3}-[0-9]{4}"))
.build();
ConstraintViolations violations = validator.validate(new ZipCode("123-4567")); // Valid
ConstraintViolations violations = validator.validate(new ZipCode(null)); // Valid
ConstraintViolations violations = validator.validate(new ZipCode("1234567")); // Invalid ("zipCode" must match [0-9]{3}-[0-9]{4})
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.12. email()
Checks if the target value is a valid email address
Validator<Email> validator = ValidatorBuilder.<Email> of()
.constraint(Email::asString, "email", c -> c.email())
.build();
ConstraintViolations violations = validator.validate(new Email("yavi@example.com")); // Valid
ConstraintViolations violations = validator.validate(new Email(null)); // Valid
ConstraintViolations violations = validator.validate(new Email("example.com")); // Invalid ("email" must not be a valid email address)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.13. password(…)
Check if the target value meets the specified password policy
Validator<Password> validator = ValidatorBuilder.<Password> of()
.constraint(Password::value, "password", c -> c.password(policy -> policy
.uppercase()
.lowercase()
// or .required(PasswordPolicy.UPPERCASE, PasswordPolicy.LOWERCASE)
.optional(1, PasswordPolicy.NUMBERS, PasswordPolicy.SYMBOLS)
.build()))
.build();
ConstraintViolations violations = validator.validate(new Password("Yavi123")); // Valid
ConstraintViolations violations = validator.validate(new Password(null)); // Valid
ConstraintViolations violations = validator.validate(new Password("yavi123")); // Invalid ("password" must meet Uppercase policy)
ConstraintViolations violations = validator.validate(new Password("yavi")); // Invalid ("password" must meet Uppercase policy, "password" must meet at least 1 policies from [Numbers, Symbols])
ConstraintViolations violations = validator.validate(new Password("")); // Invalid ("password" must meet Uppercase policy, "password" must meet Lowercase policy, "password" must meet at least 1 policies from [Numbers, Symbols])
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
|
|
|
Buit-in password policies are following:
-
am.ik.yavi.constraint.password.PasswordPolicy#UPPERCASE
-
am.ik.yavi.constraint.password.PasswordPolicy#LOWERCASE
-
am.ik.yavi.constraint.password.PasswordPolicy#ALPHABETS
-
am.ik.yavi.constraint.password.PasswordPolicy#NUMBERS
-
am.ik.yavi.constraint.password.PasswordPolicy#SYMBOLS
You can specify the count of the pattern as follows:
Validator<Password> validator = ValidatorBuilder.<Password> of()
.constraint(Password::value, "password", c -> c.password(policy -> policy
.uppercase(2) // at least 2 upper case characters are required
.lowercase(2) // at least 2 lower case characters are required
// or .required(PasswordPolicy.UPPERCASE.count(2), PasswordPolicy.LOWERCASE.count(2))
.build()))
.build();
You can define a custom password policy as bellow:
PasswordPolicy<String> passwordPolicy = new PasswordPolicy<>() {
@Override
public String name() {
return "DoNotIncludePassword";
}
@Override
public boolean test(String s) {
return !s.equalsIgnoreCase("password");
}
};
Validator<Password> validator = ValidatorBuilder.<Password> of()
.constraint(Password::value, "password", c -> c.password(policy -> policy
.required(passwordPolicy)
// ...
.build()))
.build();
7.1.14. ipv4()
Check if the target value is a valid IPv4 address
Validator<IpAddress> validator = ValidatorBuilder.<IpAddress> of()
.constraint(IpAddress::asString, "ipAddress", c -> c.ipv4())
.build();
ConstraintViolations violations = validator.validate(new IpAddress("192.0.2.1")); // Valid
ConstraintViolations violations = validator.validate(new IpAddress(null)); // Valid
ConstraintViolations violations = validator.validate(new IpAddress("example.com")); // Invalid ("ipAddress" must not be a valid IPv4)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.15. ipv6()
Check if the target value is a valid IPv6 address
Validator<IpAddress> validator = ValidatorBuilder.<IpAddress> of()
.constraint(IpAddress::asString, "ipAddress", c -> c.ipv6())
.build();
ConstraintViolations violations = validator.validate(new IpAddress("2001:0db8:bd05:01d2:288a:1fc0:0001:10ee")); // Valid
ConstraintViolations violations = validator.validate(new IpAddress(null)); // Valid
ConstraintViolations violations = validator.validate(new IpAddress("192.0.2.1")); // Invalid ("ipAddress" must not be a valid IPv6)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.16. url()
Check if the target value is a valid URL
Validator<Url> validator = ValidatorBuilder.<Url> of()
.constraint(Url::asString, "url", c -> c.url())
.build();
ConstraintViolations violations = validator.validate(new Url("https://yavi.ik.am")); // Valid
ConstraintViolations violations = validator.validate(new Url(null)); // Valid
ConstraintViolations violations = validator.validate(new Url("yavi.ik.am")); // Invalid ("url" must be a valid URL)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.17. luhn()
Checks if the digits within the target value pass the Luhn checksum algorithm (see also Luhn algorithm).
Validator<CreditCard> validator = ValidatorBuilder.<CreditCard> of()
.constraint(CreditCard::number, "creditCardNumber", c -> c.luhn())
.build();
ConstraintViolations violations = validator.validate(new CreditCard("4111111111111111")); // Valid
ConstraintViolations violations = validator.validate(new CreditCard(null)); // Valid
ConstraintViolations violations = validator.validate(new CreditCard("4111111111111112")); // Invalid (the check digit for "creditCardNumber" is invalid, Luhn checksum failed)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.18. isByte()
Check if the target value can be parsed as a Byte
value
Validator<UserId> validator = ValidatorBuilder.<UserId> of()
.constraint(UserId::asString, "userId", c -> c.isByte())
.build();
ConstraintViolations violations = validator.validate(UserId.valueOf("127")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of a byte)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.19. isShort()
Check if the target value can be parsed as a Short
value
Validator<UserId> validator = ValidatorBuilder.<UserId> of()
.constraint(UserId::asString, "userId", c -> c.isShort())
.build();
ConstraintViolations violations = validator.validate(UserId.valueOf("32767")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of a short)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.20. isInteger()
Check if the target value can be parsed as an Integer
value
Validator<UserId> validator = ValidatorBuilder.<UserId> of()
.constraint(UserId::asString, "userId", c -> c.isInteger())
.build();
ConstraintViolations violations = validator.validate(UserId.valueOf("2147483647")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of an integer)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.21. isLong()
Check if the target value can be parsed as a Long
value
Validator<UserId> validator = ValidatorBuilder.<UserId> of()
.constraint(UserId::asString, "userId", c -> c.isLong())
.build();
ConstraintViolations violations = validator.validate(UserId.valueOf("9223372036854775807")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of a long)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.22. isFloat()
Check if the target value can be parsed as a Float
value
Validator<Money> validator = ValidatorBuilder.<Money> of()
.constraint(Money::asString, "money", c -> c.isFloat())
.build();
ConstraintViolations violations = validator.validate(Money.valueOf("0.1")); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf("a")); // Invalid ("money" must be a valid representation of a float)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.23. isDouble()
Check if the target value can be parsed as a Double
value
Validator<Money> validator = ValidatorBuilder.<Money> of()
.constraint(Money::asString, "money", c -> c.isDouble())
.build();
ConstraintViolations violations = validator.validate(Money.valueOf("0.1")); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf("a")); // Invalid ("money" must be a valid representation of a double)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.24. isBigInteger()
Check if the target value can be parsed as a BigInteger
value
Validator<UserId> validator = ValidatorBuilder.<UserId> of()
.constraint(UserId::asString, "userId", c -> c.isBigInteger())
.build();
ConstraintViolations violations = validator.validate(UserId.valueOf("127")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of a big integer)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.25. isBigDecimal()
Check if the target value can be parsed as a BigDecimal
value
Validator<Money> validator = ValidatorBuilder.<Money> of()
.constraint(Money::asString, "money", c -> c.isBigDecimal())
.build();
ConstraintViolations violations = validator.validate(Money.valueOf("50.0")); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf("a")); // Invalid ("money" must be a valid representation of a big decimal)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.1.26. codePoints(…)
Checks if the target value is in the specified set of code points. Code points can be specified as allowed characters (whitelist) or prohibited characters (blacklist).
A set of code points is represented by am.ik.yavi.constraint.charsequence.CodePoints
interface, and there is am.ik.yavi.constraint.charsequence.CodePoints.CodePointsSet
interface that represents the set using java.util.Set
and am.ik.yavi.constraint.charsequence.CodePoints.CodePointsRanges
interface that represents a list of code point ranges.
For example, a code point set consisting of "A, B, C, D, a, b, c, d" is expressed as follows:
CodePointsSet<String> codePoints = () -> Set.of(
0x0041 /* A */, 0x0042 /* B */, 0x0043 /* C */, 0x0044 /* D */,
0x0061 /* a */, 0x0062 /* b */, 0x0063 /* c */, 0x0064 /* d */);
Or:
CodePointsRanges<String> codePoints = () -> List.of(
Range.of(0x0041/* A */, 0x0044 /* D */),
Range.of(0x0061/* a */, 0x0064 /* d */));
For consecutive code points, the latter is overwhelmingly more memory efficient.
If you want to regard the code point set as a white list (allowed characters), specify as follows:
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.codePoints(codePoints).asWhiteList())
.build();
ConstraintViolations violations = validator.validate(new Message("aBCd")); // Valid
ConstraintViolations violations = validator.validate(new Message(null)); // Valid
ConstraintViolations violations = validator.validate(new Message("aBCe")); // Invalid ("[e]" is/are not allowed for "text")
If you want to regard the code point set as a blacklist (prohibited characters), specify as follows:
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.codePoints(codePoints).asBlackList())
.build();
ConstraintViolations violations = validator.validate(new Message("hello")); // Valid
ConstraintViolations violations = validator.validate(new Message(null)); // Valid
ConstraintViolations violations = validator.validate(new Message("hallo")); // Invalid ("[a]" is/are not allowed for "text")
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
|
|
|
The following is a set of built-in code points.
-
am.ik.yavi.constraint.charsequence.codepoints.AsciiCodePoints#ASCII_PRINTABLE_CHARS
… ASCII printable characters -
am.ik.yavi.constraint.charsequence.codepoints.AsciiCodePoints#ASCII_CONTROL_CHARS
… ASCII control characters -
am.ik.yavi.constraint.charsequence.codepoints.AsciiCodePoints#CRLF
…0x000A
(LINE FEED) and0x000D
(CARRIAGE RETURN) -
am.ik.yavi.constraint.charsequence.codepoints.UnicodeCodePoints#HIRAGANA
… Hiragana defined in Unicode (different from JIS X 0208 definition) -
am.ik.yavi.constraint.charsequence.codepoints.UnicodeCodePoints#KATAKANA
… Katakana and Katakana Phonetic Extensions defined in Unicode (different from JIS X 0208 definition)
You can also represent the union of multiple code point sets with am.ik.yavi.constraint.charsequence.codepoints.CompositeCodePoints
class.
7.1.27. Advanced character length check
YAVI counts the input characters as the number of characters as it looks in the constraints on the number of characters. Recently, as a result of defining various characters on Unicode, the visual size and the return value of String#length
method are quite different.
YAVI supports the following character types:
-
Surrogate pair
-
Combining character
-
Variation Selector
-
IVS (Ideographic Variation Sequence)
-
SVS (Standardized Variation Sequence)
-
FVS (Mongolian Free Variation Selector)
-
-
Emoji
YAVI will perform a constraint check on surrogate pairs and combining characters with the number of characters as they look. (For Emoji, IVS, SVS and FVS, the size is not checked as it looks by default due to the performance.)
Let’s look at a typical example.
Surrogate pair
𠮷野屋
is an example of a surrogate pair. It looks like 3 characters, but the result of length
method is 4 (\uD842\uDFB7野屋
).
System.out.println("𠮷野屋".length()); // 4 (\uD842\uDFB7野屋)
Since YAVI treats the character length as the code point length, this character string is regarded as 3 characters.
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.lessThanOrEqual(3))
.build();
ConstraintViolations = validator.validate(new Message("𠮷野屋")); // Valid
Combining character
モジ
is an example of a combining character. Although it looks like 2 characters, シ
and dakuten(゙
) are combined, and the result of length()
is 3 (モシ\u3099
).
System.out.println("モジ".length()); // 3 (モシ\u3099)
YAVI considers this string to be 2 characters by default.
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.lessThanOrEqual(2))
.build();
ConstraintViolations = validator.validate(new Message("モジ")); // Valid
YAVI uses java.text.Normalizer
and normalizes with java.text.Normalizer.Form#NFC
by default. This behavior can be changed as follows: (If null
is set, it will not be normalized)
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.normalizer(normalizerForm)
.lessThanOrEqual(2))
.build();
Variation Selector
𠮟󠄀
is an example of an Ideographic Variation Sequence. Variant selectors cannot be normalized with Normalizer
.
It looks like 1 character, but when expressed in UTF-16, it is "D842 DF9F DB40 DD00", so the result of length()
is 4.
System.out.println("𠮟󠄀".length()); // 4 (\uD842\uDF9F\uDB40\uDD00󠄀)
YAVI does not consider this character as a single character by default to prevent performance degradation.
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.lessThanOrEqual(1))
.build();
ConstraintViolations = validator.validate(new Message("𠮟󠄀")); // Invalid (The size of "text" must be less than or equal to 1. The given size is 2)
You can ignore (delete) the code point of 0xE0100
-0xE01EF
, which is the range of IVS from the target string, as follows.
This way you can regard this string as just a surrogate pair.
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.variant(opts -> opts.ivs(IdeographicVariationSequence.IGNORE))
.lessThanOrEqual(1))
.build();
ConstraintViolations = validator.validate(new Message("𠮟󠄀")); // Valid
SVS and FVS can be handled in the same way.
Emoji
Emoji is crazy. The apparent number of characters and the number of code points are far away.
Let me give you examples.
System.out.println("❤️".length()); // 2
System.out.println("🤴🏻".length()); // 4
System.out.println("👨👦".length()); // 5
System.out.println("️👨👨👧👦".length()); // 12
System.out.println("🏴".length()); // 14 (WTH!)
YAVI can try to count these Emojis as one character as much as possible. There is no guarantee, but all emojis defined in Emoji 12.0 have been tested.
This process is expensive and is not enabled by default. To enable this feature, specify the emoji()
method as follows:
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.emoji().lessThanOrEqual(1))
.build();
ConstraintViolations = validator.validate(new Message("❤️")); // Valid
ConstraintViolations = validator.validate(new Message("🤴🏻")); // Valid
ConstraintViolations = validator.validate(new Message("👨👦")); // Valid
ConstraintViolations = validator.validate(new Message("️👨👨👧👦")); // Valid
ConstraintViolations = validator.validate(new Message("🏴")); // Valid
7.1.28. asByteArray()
If there is a discrepancy between the apparent character length and the actual code point length, the appearance restrictions are OK, but the size stored in the database may be exceeded. In YAVI, you can check the byte length in addition to the visual size.
Validator<Message> validator = ValidatorBuilder.<Message> of()
.constraint(Message::getText, "text", c -> c.emoji().lessThanOrEqual(3)
.asByteArray().lessThanOrEqual(16))
.build();
ConstraintViolations = validator.validate(new Message("I❤️☕️️")); // Valid
ConstraintViolations = validator.validate(new Message("❤️❤️❤️️️")); // Invalid (The byte size of "text" must be less than or equal to 16. The given size is 24)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.2. Integer/Short/Long/Character/Byte/Float/Long/BigInteger/BigDecimal
Apply the constraints on the Integer
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
.constraint(/* Function from TargetClass to Integer */, /* Field Name */, /* Constraints */)
.build();
To avoid ambiguous type inferences, you can use explicit _integer
instead of constraint
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
._integer(/* Function from TargetClass to Integer */, /* Field Name */, /* Constraints */)
.build();
The same goes for Short
/Long
/Character
/Byte
/Float
/Long
/ BigInteger
/ BigDecimal
.
7.2.1. notNull()
Checks that the target value is not null
Validator<Age> validator = ValidatorBuilder.<Age> of()
.constraint(Age::asInt, "age", c -> c.notNull())
.build();
ConstraintViolations violations = validator.validate(new Age(30)); // Valid
ConstraintViolations violations = validator.validate(new Age(null)); // Invalid ("age" must not be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.2.2. isNull()
Checks that the target value is null
Validator<Age> validator = ValidatorBuilder.<Age> of()
.constraint(Age::asInt, "age", c -> c.isNull())
.build();
ConstraintViolations violations = validator.validate(new Age(null)); // Valid
ConstraintViolations violations = validator.validate(new Age(30)); // Invalid ("age" must be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.2.3. greaterThan(…)
Checks if the target value is greater than the specified value
Validator<Age> validator = ValidatorBuilder.<Age> of()
.constraint(Age::asInt, "age", c -> c.greaterThan(20))
.build();
ConstraintViolations violations = validator.validate(new Age(21)); // Valid
ConstraintViolations violations = validator.validate(new Age(null)); // Valid
ConstraintViolations violations = validator.validate(new Age(20)); // Invalid ("age" must be greater than 20)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.2.4. greaterThanOrEqual(…)
Checks if the target value is greater than or equals to the specified value
Validator<Age> validator = ValidatorBuilder.<Age> of()
.constraint(Age::asInt, "age", c -> c.greaterThanOrEqual(20))
.build();
ConstraintViolations violations = validator.validate(new Age(20)); // Valid
ConstraintViolations violations = validator.validate(new Age(null)); // Valid
ConstraintViolations violations = validator.validate(new Age(19)); // Invalid ("age" must be greater than or equal to 10)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.2.5. lessThan(…)
Checks if the target value is less than the specified value
Validator<Age> validator = ValidatorBuilder.<Age> of()
.constraint(Age::asInt, "age", c -> c.lessThan(20))
.build();
ConstraintViolations violations = validator.validate(new Age(19)); // Valid
ConstraintViolations violations = validator.validate(new Age(null)); // Valid
ConstraintViolations violations = validator.validate(new Age(20)); // Invalid ("age" must be less than 20)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.2.6. lessThanOrEqual(…)
Checks if the target value is less than or equals to the specified value
Validator<Age> validator = ValidatorBuilder.<Age> of()
.constraint(Age::asInt, "age", c -> c.lessThanOrEqual(20))
.build();
ConstraintViolations violations = validator.validate(new Age(19)); // Valid
ConstraintViolations violations = validator.validate(new Age(null)); // Valid
ConstraintViolations violations = validator.validate(new Age(21)); // Invalid ("age" must be less than or equal to 10)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.3. Boolean
Apply the constraints on the Boolean
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
.constraint(/* Function from TargetClass to Boolean */, /* Field Name */, /* Constraints */)
.build();
To avoid ambiguous type inferences, you can use explicit _boolean
instead of constraint
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
._boolean(/* Function from TargetClass to Boolean */, /* Field Name */, /* Constraints */)
.build();
7.3.1. notNull()
Checks that the target value is not null
Validator<Confirmation> validator = ValidatorBuilder.<Confirmation> of()
.constraint(Confirmation::isConfirmed, "confirmed", c -> c.notNull())
.build();
ConstraintViolations violations = validator.validate(new Confirmation(true)); // Valid
ConstraintViolations violations = validator.validate(new Confirmation(null)); // Invalid ("confirmed" must not be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.3.2. isNull()
Checks that the target value is null
Validator<Confirmation> validator = ValidatorBuilder.<Confirmation> of()
.constraint(Confirmation::isConfirmed, "confirmed", c -> c.isNull())
.build();
ConstraintViolations violations = validator.validate(new Confirmation(null)); // Valid
ConstraintViolations violations = validator.validate(new Confirmation(true)); // Invalid ("confirmed" must be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.3.3. isTrue()
Checks that the target value is true
Validator<Confirmation> validator = ValidatorBuilder.<Confirmation> of()
.constraint(Confirmation::isConfirmed, "confirmed", c -> c.isTrue())
.build();
ConstraintViolations violations = validator.validate(new Confirmation(true)); // Valid
ConstraintViolations violations = validator.validate(new Confirmation(null)); // Valid
ConstraintViolations violations = validator.validate(new Confirmation(false)); // Invalid ("confirmed" must be true)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.3.4. isFalse()
Checks that the target value is false
Validator<Rented> validator = ValidatorBuilder.<Rented> of()
.constraint(Rented::isRented, "rented", c -> c.isFalse())
.build();
ConstraintViolations violations = validator.validate(new Rented(false)); // Valid
ConstraintViolations violations = validator.validate(new Rented(null)); // Valid
ConstraintViolations violations = validator.validate(new Rented(true)); // Invalid ("rented" must be false)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4. Collection
Apply the constraints on the Collection
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
.constraint(/* Function from TargetClass to Collection */, /* Field Name */, /* Constraints */)
.build();
To avoid ambiguous type inferences, you can use explicit _collection
instead of constraint
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
._collection(/* Function from TargetClass to Collection */, /* Field Name */, /* Constraints */)
.build();
7.4.1. notNull()
Checks that the target value is not null
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.notNull())
.build();
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Invalid ("histories" must not be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4.2. isNull()
Checks that the target value is null
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.isNull())
.build();
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Invalid ("histories" must be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4.3. notEmpty()
Checks that the target value is not empty
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.notEmpty())
.build();
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Invalid ("histories" must not be empty)
ConstraintViolations violations = validator.validate(new Histories(List.of())); // Invalid ("histories" must not be empty)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4.4. fixedSize(int)
Checks if the target value’s size is the specified size
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.fixedSize(2))
.build();
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History()))); // Invalid (The size of "histories" must be 2. The given size is 1)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4.5. greaterThan(int)
Checks if the target value’s size is greater than the specified size
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.greaterThan(1))
.build();
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History()))); // Invalid (The size of "histories" must be greater than 1. The given size is 1)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4.6. greaterThanOrEqual(int)
Checks if the target value’s size is greater than or equals to the specified size
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.greaterThanOrEqual(2))
.build();
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History()))); // Invalid (The size of "histories" must be greater than or equal to 2. The given size is 1)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4.7. lessThan(int)
Checks if the target value’s size is less than the specified size
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.lessThan(3))
.build();
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History(), new History()))); // Invalid (The size of "histories" must be less than 3. The given size is 3)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4.8. lessThanOrEqual(int)
Checks if the target value’s size is less than or equals to the specified size
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.lessThanOrEqual(2))
.build();
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History(), new History()))); // Invalid (The size of "histories" must be less than or equal to 2. The given size is 3)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.4.9. contains(…)
Checks if the target value contains the specified value
Validator<Histories> validator = ValidatorBuilder.<Histories> of()
.constraint(Histories::asList, "histories", c -> c.contains(new History(2)))
.build();
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(1), new History(2)))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(3), new History(4), new History(5)))); // Invalid ("histories" must contain History{revision=2})
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5. Map
Apply the constraints on the Map
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
.constraint(/* Function from TargetClass to Map */, /* Field Name */, /* Constraints */)
.build();
To avoid ambiguous type inferences, you can use explicit _map
instead of constraint
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
._map(/* Function from TargetClass to Map */, /* Field Name */, /* Constraints */)
.build();
7.5.1. notNull()
Checks that the target value is not null
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.notNull())
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Invalid ("codeMap" must not be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.2. isNull()
Checks that the target value is null
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.isNull())
.build();
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Invalid ("codeMap" must be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.3. notEmpty()
Checks that the target value is not empty
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.notEmpty())
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Invalid ("codeMap" must not be empty)
ConstraintViolations violations = validator.validate(new CodeMap(Map.of())); // Invalid ("codeMap" must not be empty)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.4. fixedSize(int)
Checks if the target value’s size is the specified size
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.fixedSize(2))
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of())); // Invalid (The size of "codeMap" must be 2. The given size is 1)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.5. greaterThan(int)
Checks if the target value’s size is greater than the specified size
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.greaterThan(1))
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A"))); // Invalid (The size of "codeMap" must be greater than 1. The given size is 1)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.6. greaterThanOrEqual(int)
Checks if the target value’s size is greater than or equals to the specified size
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.greaterThanOrEqual(2))
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A"))); // Invalid (The size of "codeMap" must be greater than or equal to 2. The given size is 1)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.7. lessThan(int)
Checks if the target value’s size is less than the specified size
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.lessThan(3))
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B", "c", "C"))); // Invalid (The size of "histories" must be less than 3. The given size is 3)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.8. lessThanOrEqual(int)
Checks if the target value’s size is less than or equals to the specified size
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.lessThanOrEqual(2))
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B", "c", "C"))); // Invalid (The size of "histories" must be less than or equal to 2. The given size is 3)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.9. containsKey(…)
Checks if the target value contains the specified key
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.containsKey("b"))
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("c", "C"))); // Invalid ("codeMap" must contain key b)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.5.10. containsValue(…)
Checks if the target value contains the specified value
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
.constraint(CodeMap::asMap, "codeMap", c -> c.containsValue("B"))
.build();
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("c", "C"))); // Invalid ("codeMap" must contain value B)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.6. Object
Apply the constraints on any Object
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
.constraint(/* Function from TargetClass to Object */, /* Field Name */, /* Constraints */)
.build();
To avoid ambiguous type inferences, you can use explicit _object
instead of constraint
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
._object(/* Function from TargetClass to Object */, /* Field Name */, /* Constraints */)
.build();
If you want to apply constraints on target class itself (e.g. [cross-filed-validation]), you can use constraintOnTarget
as follows:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
.constraintOnTarget(/* Field Name */, /* Constraints */)
.build();
It is equivalent to bellow:
Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
._object(Function.identity(), /* Field Name */, /* Constraints */)
.build();
7.6.1. notNull()
Checks that the target value is not null
Validator<CreatedAt> validator = ValidatorBuilder.<CreatedAt> of()
.constraint(CreatedAt::asInstant, "createdAt", c -> c.notNull())
.build();
ConstraintViolations violations = validator.validate(new CreatedAt(Instant.now())); // Valid
ConstraintViolations violations = validator.validate(new CreatedAt(null)); // Invalid ("createdAt" must not be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.6.2. isNull()
Checks that the target value is null
Validator<CreatedAt> validator = ValidatorBuilder.<CreatedAt> of()
.constraint(CreatedAt::asInstant, "createdAt", c -> c.isNull())
.build();
ConstraintViolations violations = validator.validate(new CreatedAt(null)); // Valid
ConstraintViolations violations = validator.validate(new CreatedAt(Instant.now())); // Invalid ("createdAt" must be null)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
7.6.3. password()
Check if the target value meets the specified custom password policy (e.g. Cross-field check)
PasswordPolicy<Account> passwordPolicy = PasswordPolicy.of("DoNotIncludeUsername",
account -> {
String username = account.getUsername();
String password = account.getPassword();
if (username == null || password == null) {
return true;
}
return !password.toUpperCase().contains(username.toUpperCase());
});
Validator<Account> validator = ValidatorBuilder.<Account> of()
.constraintOnTarget("password", c -> c.password(policy -> policy
.required(passwordPolicy)
.build()))
.build();
ConstraintViolations violations = validator.validate(new Account("foo", "Bar1234")); // Valid
ConstraintViolations violations = validator.validate(new Account(null, null)); // Valid
ConstraintViolations violations = validator.validate(new Account("foo", "Foo1234")); // Invalid ("password" must meet DoNotIncludeUsername policy)
Message Key | Default Message Format | Args |
---|---|---|
|
|
|
|
|
|
8. Annotation Processor
YAVI provides Annotation Processor to generates classes which defines meta classes
that implement am.ik.yavi.meta.ConstraintMeta
for each target which is annotated with am.ik.yavi.meta.ConstraintTarget
or am.ik.yavi.meta.ConstraintArguments
.
When you have a class as following, run mvn clean compile
:
package com.example;
import am.ik.yavi.meta.ConstraintTarget;
public class UserForm {
private String name;
private String email;
private Integer age;
@ConstraintTarget
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ConstraintTarget
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@ConstraintTarget
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
You’ll see target/generated-sources/annotations/com/example/_UserFormMeta.java
is automatically generated.
|
8.1. Annotating @ConstraintTarget
@ConstraintTarget
is used to generate meta classes that can be used with ValidatorBuilder#constraint
.
+ It can be annotated on methods or constructor arguments.
Xyz
class that is annotated with @ConstraintTarget
will generate _XyzMeta
.
Validator
can be built as follows:
Validator<UserForm> validator = ValidatorBuilder.<UserForm> of()
.constraint(_UserFormMeta.NAME, c -> c.notEmpty())
.constraint(_UserFormMeta.EMAIL, c -> c.notEmpty().email())
.constraint(_UserFormMeta.AGE, c -> c.greaterThan(0))
.build();
No more string literal is required and it’s really type-safe.
The example above is available for the following 4 ways of generation.
8.1.1. @ConstraintTarget
on getters
If you like "Java Beans" style, this way would be straightforward.
package com.example;
import am.ik.yavi.meta.ConstraintTarget;
public class UserForm {
private String name;
private String email;
private Integer age;
@ConstraintTarget
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ConstraintTarget
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@ConstraintTarget
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
will generate
package com.example;
// Generated at 2020-02-11T17:37:47.866300+09:00
public class _UserFormMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> NAME = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "name";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
return com.example.UserForm::getName;
}
};
public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "email";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
return com.example.UserForm::getEmail;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "age";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.Integer> toValue() {
return com.example.UserForm::getAge;
}
};
}
8.1.2. on constructor arguments + getters
If you prefer an immutable class like Value Object, annotate @ConstraintTarget
on constructor arguments.
package com.example;
import am.ik.yavi.meta.ConstraintTarget;
public class UserForm {
private final String name;
private final String email;
private final Integer age;
public UserForm(@ConstraintTarget String name,
@ConstraintTarget String email,
@ConstraintTarget Integer age) {
this.name = name;
this.email = email;
this.age = age;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public Integer getAge() {
return age;
}
}
will generate
package com.example;
// Generated at 2020-02-11T17:59:03.892823+09:00
public class _UserFormMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> NAME = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "name";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
return com.example.UserForm::getName;
}
};
public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "email";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
return com.example.UserForm::getEmail;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "age";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.Integer> toValue() {
return com.example.UserForm::getAge;
}
};
}
This is as same as the first example.
8.1.3. on constructor arguments + non-getters
You may not like "getter" style for immutable objects.
You can use filed name as method name using getter = false
. This will be a great fit for Records.
package com.example;
import am.ik.yavi.meta.ConstraintTarget;
public class UserForm {
private final String name;
private final String email;
private final Integer age;
public UserForm(@ConstraintTarget(getter = false) String name,
@ConstraintTarget(getter = false) String email,
@ConstraintTarget(getter = false) Integer age) {
this.name = name;
this.email = email;
this.age = age;
}
public String name() {
return name;
}
public String email() {
return email;
}
public Integer age() {
return age;
}
}
will generate
package com.example;
// Generated at 2020-02-11T18:00:37.868495+09:00
public class _UserFormMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> NAME = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "name";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
return com.example.UserForm::name;
}
};
public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "email";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
return com.example.UserForm::email;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "age";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.Integer> toValue() {
return com.example.UserForm::age;
}
};
}
8.1.4. on constructor arguments + field access
You may prefer accessing fields directly rather than accessors, then use field = true
.
package com.example;
import am.ik.yavi.meta.ConstraintTarget;
public class UserForm {
final String name;
final String email;
final Integer age;
public UserForm(@ConstraintTarget(field = true) String name,
@ConstraintTarget(field = true) String email,
@ConstraintTarget(field = true) Integer age) {
this.name = name;
this.email = email;
this.age = age;
}
}
will generate
package com.example;
// Generated at 2020-02-11T18:02:47.124191+09:00
public class _UserFormMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> NAME = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "name";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
return x -> x.name;
}
};
public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "email";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
return x -> x.email;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm>() {
@Override
public String name() {
return "age";
}
@Override
public java.util.function.Function<com.example.UserForm, java.lang.Integer> toValue() {
return x -> x.age;
}
};
}
8.2. Annotating @ConstraintArguments
@ConstraintArguments
is used to generate meta classes that can be used with ArgumentsValidatorBuilder
.
+ It can be annotated on constructors or methods.
The constructor of Xyz
class that is annotated with @ConstraintTarget
will generate _XyzArgumentsMeta
.
doSomething
method of Xyz
class that is annotated with @ConstraintTarget
will generate _XyzDoSomethingArgumentsMeta
.
8.2.1. Validating Constructor Arguments
package com.example;
import am.ik.yavi.meta.ConstraintArguments;
public class User {
private final String name;
private final String email;
private final int age;
@ConstraintArguments
public User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
}
will generate
package com.example;
// Generated at 2020-02-11T18:22:15.164882+09:00
public class _UserArgumentsMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>> NAME = new am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>>() {
@Override
public String name() {
return "name";
}
@Override
public java.util.function.Function<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>, java.lang.String> toValue() {
return am.ik.yavi.arguments.Arguments1::arg1;
}
};
public static final am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>>() {
@Override
public String name() {
return "email";
}
@Override
public java.util.function.Function<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>, java.lang.String> toValue() {
return am.ik.yavi.arguments.Arguments2::arg2;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>>() {
@Override
public String name() {
return "age";
}
@Override
public java.util.function.Function<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>, java.lang.Integer> toValue() {
return am.ik.yavi.arguments.Arguments3::arg3;
}
};
}
ArgumentsNValidator
can be built as follows:
Arguments3Validator<String, String, Integer, User> validator = ArgumentsValidatorBuilder
.of(User::new)
.builder(b -> b
.constraint(_UserArgumentsMeta.NAME, c -> c.notEmpty())
.constraint(_UserArgumentsMeta.EMAIL, c -> c.notEmpty().email())
.constraint(_UserArgumentsMeta.AGE, c -> c.greaterThan(0))
)
.build();
8.2.2. Validating Method Arguments
package com.example;
import am.ik.yavi.meta.ConstraintArguments;
public class UserService {
@ConstraintArguments
public User createUser(String name, String email, int age) {
return new User(name, email, age);
}
}
will generate
package com.example;
// Generated at 2020-02-11T18:40:53.554587+09:00
public class _UserServiceCreateUserArgumentsMeta {
public static final am.ik.yavi.meta.ObjectConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, com.example.UserService> USERSERVICE = new am.ik.yavi.meta.ObjectConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, com.example.UserService>() {
@Override
public String name() {
return "userService";
}
@Override
public java.util.function.Function<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, com.example.UserService> toValue() {
return am.ik.yavi.arguments.Arguments1::arg1;
}
};
public static final am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>> NAME = new am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>>() {
@Override
public String name() {
return "name";
}
@Override
public java.util.function.Function<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, java.lang.String> toValue() {
return am.ik.yavi.arguments.Arguments2::arg2;
}
};
public static final am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>>() {
@Override
public String name() {
return "email";
}
@Override
public java.util.function.Function<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, java.lang.String> toValue() {
return am.ik.yavi.arguments.Arguments3::arg3;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>>() {
@Override
public String name() {
return "age";
}
@Override
public java.util.function.Function<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, java.lang.Integer> toValue() {
return am.ik.yavi.arguments.Arguments4::arg4;
}
};
}
ArgumentsNValidator
can be built as follows:
Arguments4Validator<UserService, String, String, Integer, User> validator = ArgumentsValidatorBuilder
.of(UserService::createUser)
.builder(b -> b
.constraint(_UserServiceCreateUserArgumentsMeta.USERSERVICE, c -> c.notNull())
.constraint(_UserServiceCreateUserArgumentsMeta.NAME, c -> c.notEmpty())
.constraint(_UserServiceCreateUserArgumentsMeta.EMAIL, c -> c.notEmpty().email())
.constraint(_UserServiceCreateUserArgumentsMeta.AGE, c -> c.greaterThan(0))
)
.build();
9. Integrations
Although YAVI doesn’t depend on anything, it has interfaces that magically fit into Framework.
9.1. Integration with BindingResult
in Spring MVC
ConstraintViolations#apply
accepts the Function Interface with the same arguments as Spring Framework’s BindingResult#rejectValue
method and passes the violation messages.
To reflect validation results by YAVI in a Controller of Spring MVC, you can write the following code.
final Validator<UserForm> validator = ...;
@PostMapping(path = "users")
public String createUser(Model model, UserForm userForm, BindingResult bindingResult) {
ConstraintViolations violations = validator.validate(userForm);
if (!violations.isValid()) {
violations.apply(bindingResult::rejectValue);
return "userForm";
}
// ...
return "redirect:/";
}
If you like a functional way, you can also write:
final Validator<UserForm> validator = ...;
@PostMapping(path = "users")
public String createUser(Model model, UserForm userForm, BindingResult bindingResult) {
return validator.applicative()
.validate(userForm)
.fold(violations -> {
ConstraintViolations.of(violations).apply(bindingResult::rejectValue);
return "userForm";
}, form -> {
// ...
return "redirect:/";
});
}
List<ConstraintViolation> is passed to the function of the first argument of Validated#fold instead of ConstraintViolations . You can convert the List<ContraintViolation> to ConstraintViolations by using the ConstraintViolations#of method.
|
https://github.com/making/demo-spring-mvc-yavi is a full example. |
9.2. Converting ConstraintViolations
to the format to serialize as a response body
ConstraintViolation
is not suitable for serializing with a serializer like Jackson. Instead, you can use ConstraintViolation#detail
method to convert it to a ViolationDetail
object that is easy to serialize.
The ConstraintViolations#details
method translates all ConstraintViolation
s and returns List<ViolationDetail>
.
final Validator<UserCreateRequest> validator = ...;
@PostMapping(path = "users")
public ResponseEntity<?> createUser(@RequestBody UserCreateRequest request) {
ConstraintViolations violations = validator.validate(request);
if (violations.isValid()) {
User created = userService.create(request.toUser());
return ResponseEntity.ok(created);
} else {
return ResponseEntity.badRequest().body(violations.details());
}
}
If you like a functional way, you can also write:
final Validator<UserCreateRequest> validator = ...;
@PostMapping(path = "users")
public ResponseEntity<?> createUser(@RequestBody UserCreateRequest request) {
return validator.applicative()
.validate(request)
.map(req -> userService.create(req.toUser()))
.mapErrors(violations -> ConstraintViolations.of(violations).details())
// or .mapError(ConstraintViolation::detail)
.fold(details -> ResponseEntity.badRequest().body(details),
created -> ResponseEntity.ok(created));
}
ViolationDetail works with GraalVM native image out of the box.
|
9.3. Integration with Spring WebFlux.fn
YAVI will be a great fit for Spring WebFlux.fn.
final Validator<UserCreateRequest> validator = ...;
public RouterFunction<ServerResponse> routes() {
return RouterFunctions.route()
.POST("/users", request -> request.bodyToMono(UserCreateRequest.class)
.flatMap(body -> validator.applicative()
.validate(body)
.map(req -> userService.create(req.toUser()))
.mapErrors(violations -> ConstraintViolations.of(violations).details())
// or .mapError(ConstraintViolation::detail)
.fold(details -> ServerResponse.badRequest().bodyValue(details),
created -> ServerResponse.ok().bodyValue(created))))
.build();
}
YAVI was originally developed as a validator naturally fit with Spring WebFlux.fn. |
9.4. Integration with MessageSource
in Spring Framework
am.ik.yavi.message.MessageSourceMessageFormatter
accepts the Functional Interface with the same arguments as Spring Framework’s MessageSource#getMessage
.
This allows you to delegate control of the message format of violation messages to Spring Framework.
MessageSourceMessageFormatter
can be used as follows:
@RestController
public class OrderController {
private final Validator<CartItem> validator;
public OrderController(MessageSource messageSource) {
MessageFormatter messageFormatter = new MessageSourceMessageFormatter(messageSource::getMessage);
this.validator = ValidatorBuilder.<CartItem> of()
.constraints(...)
.messageFormatter(messageFormatter)
.build();
}
}
9.5. Managing ValidatorBuilder
in an IoC Container
If you want to customize ValidatorBuilder
and manage it with an IoC Container like Spring Framework, you can use am.ik.yavi.factory.ValidatorFactory
.
The following is an example of defining a ValidatorFactory
in Spring Framework:
@Bean
public ValidatorFactory yaviValidatorFactory(MessageSource messageSource) {
MessageFormatter messageFormatter = new MessageSourceMessageFormatter(messageSource::getMessage);
return new ValidatorFactory("." /* Message Key Separator */, messageFormatter);
}
The usage of a Validator would look like following:
@RestController
public class OrderController {
private final Validator<CartItem> validator;
public OrderController(ValidatorFactory factory) {
this.validator = factory.validator(builder -> builder.constraint(...));
}
}
9.6. Obtaining a BiValidator
am.ik.yavi.core.BiValidator<T, E>
is a BiConsumer<T, E>
.
T
is the type of target object as usual and E
is the type of errors object.
This class is helpful for libraries or apps to adapt both YAVI and other validation framework that accepts these two arguments like Spring Framework’s org.springframework.validation.Validator#validate(Object, Errors)
.
BiValidator
can be obtained as below:
BiValidator<CartItem, Errors> validator = ValidatorBuilder.<CartItem> of()
.constraint(...)
.build(Errors::rejectValue);
There is a factory for BiValidator
as well
@Bean
public BiValidatorFactory<Errors> biValidatorFactory() {
return new BiValidatorFactory<>(Errors::rejectValues);
}
or, if you want to customize the builder
@Bean
public BiValidatorFactory<Errors> biValidatorFactory(MessageSource messageSource) {
MessageFormatter messageFormatter = new MessageSourceMessageFormatter(messageSource::getMessage);
return new BiValidatorFactory<>("." /* Message Key Separator */, messageFormatter, Errors::rejectValues);
}
The usage of a BiValidator
would look like following:
@RestController
public class OrderController {
private final BiValidator<CartItem, Errors> validator;
public OrderController(BiValidatorFactory<Errors> factory) {
this.validator = factory.validator(builder -> builder.constraint(...));
}
}