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, including temporal and enum constraints

  • Easy custom constraints

  • Validation for constraint groups and contexts, conditional validation, and conflict strategy

  • Validation for arguments before creating an object, and the "Always Valid model" that makes invalid objects impossible to create

  • Kotlin integration, including coroutine extensions

  • 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

Welcome to YAVI.

The following paragraphs will guide you through the initial steps required to integrate YAVI into your application.

2.1. Prerequisites

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.17.3</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.14.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.27.7</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 on manufacturer is violated in manufacturerIsNull()

  • The greaterThanOrEqual(int) constraint on licensePlate is violated in licensePlateTooShort()

  • The greaterThanOrEqual(int) constraint on seatCount is violated in seatCountTooLow()

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.

2.5. The Always Valid model

The examples above create a Car instance and then validate it, so an invalid Car can exist temporarily. YAVI can also make invalid objects impossible to create by validating the arguments before the constructor runs and returning the result only when all arguments are valid.

Make the constructor private and expose a static factory method that returns Validated<Car>:

package com.example;

import am.ik.yavi.arguments.Arguments3Validator;
import am.ik.yavi.core.Validated;
import am.ik.yavi.validator.Yavi;

public final class Car {

    private static final Arguments3Validator<String, String, Integer, Car> validator = Yavi.arguments()
        ._string("manufacturer", c -> c.notNull())
        ._string("licensePlate", c -> c.notNull().greaterThanOrEqual(2).lessThanOrEqual(14))
        ._integer("seatCount", c -> c.greaterThanOrEqual(2))
        .apply(Car::new);

    private final String manufacturer;

    private final String licensePlate;

    private final Integer seatCount;

    public static Validated<Car> of(String manufacturer, String licensePlate, Integer seatCount) {
        return validator.validate(manufacturer, licensePlate, seatCount);
    }

    private Car(String manufacturer, String licensePlate, Integer seatCount) {
        this.manufacturer = manufacturer;
        this.licensePlate = licensePlate;
        this.seatCount = seatCount;
    }

    public String manufacturer() {
        return manufacturer;
    }

    public String licensePlate() {
        return licensePlate;
    }

    public Integer seatCount() {
        return seatCount;
    }
}

Car.of(…​) validates the arguments and returns the result wrapped in Validated. Because the constructor is private, a Car can only be obtained from the validated result. When validation fails, inspect the violations with Validated#errors():

Validated<Car> validated = Car.of(null, "DD-AB-123", 4);
if (!validated.isValid()) {
    ConstraintViolations violations = validated.errors();
    // ...
}

If you prefer a record and want validation inside the constructor, use lazy() and call validated(…​) from the compact constructor:

package com.example;

import am.ik.yavi.arguments.Arguments3Validator;
import am.ik.yavi.validator.Yavi;

public record Car(String manufacturer, String licensePlate, Integer seatCount) {

    private static final Arguments3Validator<String, String, Integer, Car> validator = Yavi.arguments()
        ._string("manufacturer", c -> c.notNull())
        ._string("licensePlate", c -> c.notNull().greaterThanOrEqual(2).lessThanOrEqual(14))
        ._integer("seatCount", c -> c.greaterThanOrEqual(2))
        .apply(Car::new);

    public Car {
        validator.lazy().validated(manufacturer, licensePlate, seatCount);
    }
}

lazy() returns a validator that produces a Supplier instead of invoking the mapper, so the validation runs without calling Car::new again. Without lazy(), the validator would construct a Car while the constructor is still running and recurse until a StackOverflowError.

Invalid arguments make the compact constructor throw ConstraintViolationsException. Its violations() method returns the violations:

assertThatThrownBy(() -> new Car(null, "DD-AB-123", 4))
    .isInstanceOf(ConstraintViolationsException.class)
    .satisfies(e -> {
        ConstraintViolations violations = ((ConstraintViolationsException) e).violations();
        assertThat(violations).hasSize(1);
    });

See Validating arguments for the Yavi.arguments() builder and the other ways to define arguments validators.

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 Set and regard it as a collection as follows

ToCollection<CodeMap, Collection<String>, String> toCollection = codeMap -> codeMap.asMap().keySet();
Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
    .forEach(toCollection, "codeMap", b -> b._string(String::toString, "value", c -> c.notEmpty()))
    .build();

3.4. Applying constraints only to specific conditions

You can apply constraints only to specific conditions with am.ik.yavi.core.ConstraintCondition. Since 0.11.0 a condition receives an am.ik.yavi.core.ConstraintContext instead of a ConstraintGroup, so it can read arbitrary attributes of the validation call:

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraintOnCondition((user, context) -> !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.

A ConstraintContext can be created from a Map or from a lookup function, and passed to the validation method:

ConstraintContext context = ConstraintContext.from(Map.of("country", "IT"));
// or
ConstraintContext context = ConstraintContext.from(key -> lookupAttribute(key));

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraintOnCondition((user, ctx) -> "IT".equals(ctx.attribute("country").value()),
        b -> b.constraint(User::getTaxId, "taxId", c -> c.notBlank()))
    .build();

ConstraintViolations violations = validator.validate(user, context);

ConstraintCondition.hasAttribute(String) and ConstraintCondition.hasAttributeWithValue(String, Object) are shortcuts for the common attribute checks:

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraintOnCondition(ConstraintCondition.hasAttributeWithValue("country", "IT"),
        b -> b.constraint(User::getTaxId, "taxId", c -> c.notBlank()))
    .build();

If the attribute is absent, context.attribute("key").value() returns null.

3.5. Applying constraints only to specific groups

A ConstraintGroup is a ConstraintContext that only carries a name. You can apply constraints only to specific groups as follows:

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();

A group can also be created from a name without declaring an enum:

ConstraintGroup create = ConstraintGroup.of("CREATE");

The group to validate can be specified in validate method:

ConstraintViolations violations = validator.validate(user, ConstraintGroup.of("CREATE"));
// or, with the enum above
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 validate method, ConstraintGroup.DEFAULT is used. Groups match by name; two groups are equal when their names are equal.

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();

message also accepts an am.ik.yavi.core.ViolationMessage that carries the message key and the default message format, for example ViolationMessage.of("string.isbn13", "\"{0}\" must be ISBN13 format"). Since 0.17.2, overriding the message of a constraint preserves the violated value in the message arguments.

3.9. Value validators

am.ik.yavi.core.ValueValidator validates an input and produces a validated value. It is the applicative counterpart of Validator; see "Combining validators" for combining value validators and "Combining validation results" for the Validated result type.

A value validator maps the validated value with map:

ValueValidator<String, String> trimmed = ValueValidator.<String> passThrough().map(String::trim);

andThen(Function) is deprecated since 0.17.0; use map(Function) instead. andThen(ValueValidator) is still supported and chains validators, applying the next validator to the value produced by the previous one:

ValueValidator<String, Integer> length = trimmed
    .andThen(ValueValidator.<String> passThrough().map(String::length));

validated(…​) validates the input and returns the value, throwing am.ik.yavi.core.ConstraintViolationsException when a violation exists. The same behavior is available through Validation#throwIfInvalid and ConstraintViolations#throwIfInvalid:

try {
    String value = trimmed.validated(input);
}
catch (ConstraintViolationsException e) {
    // handle the violations
}

ConstraintViolations violations = validator.validate(user);
violations.throwIfInvalid(ConstraintViolationsException::new);

A value validator can be converted to a regular Validator with Validatable.from(ValueValidator), which discards the produced value and only reports the violations:

Validatable<String> validator = Validatable.from(trimmed);

3.10. Handling violation results

ConstraintViolation has a staged builder since 0.15.0. The name, messageKey and defaultMessageFormat stages are required; the argument, formatter and locale methods are optional:

ConstraintViolation violation = ConstraintViolation.builder()
    .name("email")
    .messageKey("email.invalid")
    .defaultMessageFormat("\"{0}\" must be a valid email address")
    .argsWithPrependedNameAndAppendedViolatedValue(new Object[] {}, new ViolatedValue(input))
    .messageFormatter(SimpleMessageFormatter.getInstance())
    .locale(Locale.JAPAN)
    .build();

ConstraintViolation#violatedValue() returns the value that violated the constraint.

SimpleMessageFormatter can be obtained with SimpleMessageFormatter.getInstance(); the public no-arg constructor is still available but is deprecated since 0.16.0.

3.11. 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.MessageFormat to interpolate the message. A shared instance is available with SimpleMessageFormatter.getInstance(); the public no-arg constructor is kept for backward compatibility but is deprecated. 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:

  • Include field name in error message by default

  • Allows you to include the violated values in the error message

The first placeholder {0} of the message is set to the field name, and the last placeholder is set to the violation value.

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.

44784067 4b010600 abc7 11e8 8878 930d017405bb

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.12. 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);

The failFast(boolean) and isFailFast() methods are also declared on am.ik.yavi.core.Validatable, so the mode can be read and switched through that interface:

boolean failFast = failFastValidator.isFailFast();
Validatable<User> validatable = validator.failFast(true);

Since 0.13.0 nested, collection and conditional validators honor the parent’s fail-fast mode. The mode is passed down to nested validators and element validators, so the validation stops at the first violation in those validators as well.

3.13. Resolving conflicting constraint names

When the same constraint name is registered more than once, ValidatorBuilder#conflictStrategy(ConflictStrategy) decides what happens when the validator is built. The default, am.ik.yavi.builder.ConflictStrategy.NOOP, keeps the existing constraints and appends the new one, so the target is validated by all of them. ConflictStrategy.OVERRIDE removes the existing constraints and keeps only the new one:

Validator<User> validator = ValidatorBuilder.<User> of()
    .conflictStrategy(ConflictStrategy.OVERRIDE)
    .constraint(User::getName, "name", c -> c.notNull())
    .constraint(User::getName, "name", c -> c.notBlank()) // replaces the previous "name" constraint
    .build();

A custom strategy can mutate the list of existing predicates; the new predicate is appended after the strategy returns:

ConflictStrategy keepFirst = new ConflictStrategy() {
    @Override
    public <T> void resolveConflict(List<ConstraintPredicates<T, ?>> predicatesList,
            ConstraintPredicates<T, ?> predicates) {
        while (predicatesList.size() > 1) {
            predicatesList.remove(predicatesList.size() - 1);
        }
    }
};

3.14. Kotlin Support

If you are using Kotlin, you can define a Validator with DSL as follows. The DSL obtains property names with Kotlin reflection, so kotlin-reflect must be on the runtime classpath.

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
}

nestIfPresent and forEachIfPresent validate nullable nested objects, collections, maps and arrays only when they are present:

val validator = validator<Order> {
    Order::customer nestIfPresent {
        Customer::name {
            notBlank()
        }
    }
    Order::items forEachIfPresent {
        OrderItem::name {
            notBlank()
        }
    }
}

Temporal and enum properties have their own overloads. For example, LocalDate, LocalTime, LocalDateTime, ZonedDateTime, OffsetDateTime, Instant, Year and YearMonth properties can use the date and time constraints, and enum properties can use oneOf. See "Built-in Constraints" for the available constraints:

enum class Status { ACTIVE, INACTIVE }

val validator = validator<Account> {
    Account::createdAt {
        notNull()
        past()
    }
    Account::status {
        notNull()
        oneOf(Status.ACTIVE)
    }
}

onCondition and onGroup add conditional and group-specific constraints, and unwrap gives access to the underlying ValidatorBuilder:

val validator = validator<User> {
    onCondition({ _, context -> "IT".equals(context.attribute("country").value()) }) {
        User::taxId {
            notNull()
        }
    }
    onGroup(ConstraintGroup.of("CREATE")) {
        User::id {
            isNull()
        }
    }
    unwrap().failFast(true)
}

3.14.1. Coroutines support

The extensions in am.ik.yavi.fn.ValidationExtensions suspend the validation result handling. awaitFold, awaitBimap, awaitMap, awaitMapErrors and awaitMapError correspond to fold, bimap, map, mapErrors and mapError. They require kotlinx-coroutines-core:

import am.ik.yavi.fn.awaitFold

suspend fun createUser(validated: Validated<User>): HttpStatus = validated.awaitFold(
    errorsMapper = { violations -> HttpStatus.BAD_REQUEST },
    mapper = { user -> HttpStatus.OK },
)

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));

Validation#throwIfInvalid and ConstraintViolations#throwIfInvalid throw the exception supplied by their function argument instead of returning a value:

emailValidated.throwIfInvalid(ConstraintViolationsException::new);

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);

For Validated, the error type of fold is List<ConstraintViolation>.

4.2. Mapping and inspecting validation results

Validation provides map, flatMap, mapError, mapErrors, bimap, orElseThrow and throwIfInvalid to transform or unwrap the result:

Validation<ConstraintViolation, String> mapped = emailValidated.map(Email::value);
Validation<String, Email> mappedErrors = emailValidated.mapError(ConstraintViolation::messageKey);
Validation<String, String> bimapped = emailValidated
    .bimap(violations -> List.of("invalid"), Email::value);

am.ik.yavi.core.Validated is a shortcut of Validation<ConstraintViolation, T>. It can be created with Validated.successWith, Validated.failureWith and Validated.of:

Validated<Email> success = Validated.successWith(new Email("yavi@example.com"));
Validated<Email> failure = Validated.failureWith(violations); // ConstraintViolations is a List<ConstraintViolation>
Validated<Email> fromValidation = Validated.of(Validation.success(new Email("yavi@example.com")));

Multiple results can be combined with Validated.sequence, Validated.traverse, Validated.traverseIndexed and Validated.traverseOptional:

Validated<List<Email>> emails = Validated.sequence(List.of(emailValidated, anotherEmailValidated));
Validated<List<Email>> traversed = Validated.traverse(inputs, emailValidator::validate);
Validated<List<Email>> indexed = Validated.traverseIndexed(inputs,
    (input, index) -> emailValidator.indexed(index).validate(input));
Validated<Optional<Email>> optional = Validated.traverseOptional(optionalInput, emailValidator::validate);

ConstraintViolations#of(List<ConstraintViolation>) creates a ConstraintViolations from a list of violations.

4.3. 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));

Validations.combine(…​) supports 2 to 16 validations.

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 ConstraintViolation s:

* "email" must not be blank
* "email" must be a valid email address
* "phoneNumber" must match [0-9\-]+

Validation for ContactInfo in the above example can be achieved by using nest method as follows:

Validator<Email> emailValidator = ValidatorBuilder.<Email> of()
    .constraint(Email::value, "value", c -> c.notBlank().email())
    .build();
Validator<PhoneNumber> phoneNumberValidator = ValidatorBuilder.<PhoneNumber> of()
    .constraint(PhoneNumber::value, "value", c -> c.notBlank().pattern("[0-9\\-]+"))
    .build();

Validator<ContactInfo> validator = ValidatorBuilder.<ContactInfo> of()
    .nest(ContactInfo::email, "email", emailValidator)
    .nest(ContactInfo::phoneNumber, "phoneNumber", phoneNumberValidator)
    .build();

Alternatively, you can combine ApplicativeValidator<Email> and ApplicativeValidator<PhoneNumber> as introduced in Combining validators.

Arguments2Validator<Email, PhoneNumber, ContactInfo> contactInfoValidator = ArgumentsValidators
    .split(emailValidator.applicative(), phoneNumberValidator.applicative())
    .apply(ContactInfo::new);

Validated<ContactInfo> contactInfoValidated = contactInfoValidator.validate(new Email("yavi@example.com"), new PhoneNumber("090-123-4567"));

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.

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. The Always Valid model shows how to combine this with a private constructor or a record to make invalid objects impossible to create.

5.1. Defining an arguments validator

Use am.ik.yavi.validator.Yavi#arguments() to build an arguments validator. Each _xxx method adds one argument with its name and constraints, and apply creates the validator:

import am.ik.yavi.arguments.Arguments3Validator;
import am.ik.yavi.core.Validated;
import am.ik.yavi.validator.Yavi;

public class Person {

    public static final Arguments3Validator<String, String, Integer, Person> validator = Yavi.arguments()
        ._string("name", c -> c.notBlank().lessThanOrEqual(100)) // Constraint on the first argument of Person::new
        ._string("email", c -> c.notBlank().lessThanOrEqual(100).email()) // Constraint on the second argument of Person::new
        ._integer("age", c -> c.greaterThanOrEqual(0).lessThan(200)) // Constraint on the third argument of Person::new
        .apply(Person::new);

    // ...
}

The Arguments Validator can be used as follows:

Validated<Person> personValidated = Person.validator.validate("John Doe", "jdoe@example.com", 30);

am.ik.yavi.arguments.Arguments1Validator to am.ik.yavi.arguments.Arguments16Validator are available. The maximum number of arguments is 16.

5.2. Available argument kinds

The following argument kinds are available, each with a constraint type for that kind:

  • _string

  • _boolean

  • _short, _integer, _long, _float, _double

  • _bigInteger, _bigDecimal

  • _localDate, _localTime, _localDateTime, _offsetDateTime, _instant, _year, _yearMonth, _zonedDateTime

  • _enum

  • _object

_object accepts any reference type, including arrays, collections and maps:

Arguments1Validator<LocalDate, LocalDate> dateValidator = Yavi.arguments()
    ._localDate("date", c -> c.notNull())
    .apply(date -> date);

Arguments1ValidatorBuilder<Color, Color> colorValidatorBuilder = Yavi.arguments()
    ._enum("color", c -> c.notNull());
Arguments1Validator<Color, Color> colorValidator = colorValidatorBuilder.apply(color -> color);

Arguments1Validator<List<String>, List<String>> tagsValidator = Yavi.arguments()
    ._object(ObjectValidatorBuilder.<List<String>>of("tags", c -> c.notNull()).build())
    .apply(tags -> tags);

_collection, _map and _objectArray are not argument kinds; they are ValidatorBuilder methods for validating a collection, map or array property of an object. See Using YAVI for details.

5.3. 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 with ArgumentsValidatorBuilder:

Arguments3Validator<UserService, String, String, User> validator = ArgumentsValidatorBuilder
    .of(UserService::createUser)
    .builder(b -> b
        ._object(Arguments3::arg1, "userService", c -> c.notNull())
        ._string(Arguments3::arg2, "email", c -> c.email())
        ._string(Arguments3::arg3, "name", c -> c.notBlank()))
    .build();

UserService userService = new UserService();
Validated<User> userValidated = validator.validate(userService, "jdoe@example.com", "John Doe");

Use this form when the values come from an instance method reference, because the receiver is validated as the first argument. It is also the form generated by the annotation processor meta constants described in Annotation Processor. The _xxx methods of the builder take the accessor for the argument position (Arguments3::arg1 etc.) instead of just a name; Arguments1Validator..Arguments16Validator do not inherit from each other, so use the accessor that matches the arity of the builder.

void cannot be used as return type while java.lang.Void is available.

5.4. 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();

IntegerValidator<Integer> ageValidator = IntegerValidatorBuilder
    .of("age", c -> c.greaterThanOrEqual(0).lessThan(200))
    .build();

Validated<String> nameValidated = nameValidator.validate("John Doe");
Validated<Integer> ageValidated = ageValidator.validate(30);

build(Function) converts the validated value into a Value Object:

StringValidator<Name> nameValidator = StringValidatorBuilder
    .of("name", c -> c.notBlank().lessThanOrEqual(100))
    .build(Name::new);

StringValidator<Email> emailValidator = StringValidatorBuilder
    .of("email", c -> c.notBlank().lessThanOrEqual(100).email())
    .build(Email::new);

IntegerValidator<Age> ageValidator = IntegerValidatorBuilder
    .of("age", c -> c.greaterThanOrEqual(0).lessThan(200))
    .build(Age::new);

You can also map an existing validator with map(Function):

StringValidator<Name> nameValidator = StringValidatorBuilder
    .of("name", c -> c.notBlank().lessThanOrEqual(100))
    .build()
    .map(Name::new);

andThen(Function) produces the same result and is deprecated since 0.17.0 in favor of map(Function). andThen(ValueValidator) is not deprecated; it chains another validator on the produced value:

ObjectValidator<Name, Name> nonNullNameValidator = ObjectValidatorBuilder
    .<Name>of("name", c -> c.notNull())
    .build();

Arguments1Validator<String, Name> checkedNameValidator = nameValidator
    .andThen(nonNullNameValidator);

5.5. Composing and adapting validators

compose(Function) converts the source object before validation:

Arguments1Validator<Integer, Name> nameFromIdValidator = nameValidator.compose(Object::toString);

wrap() turns an arguments validator into one that validates an ArgumentsN container, and Arguments1Validator.unwrap(…​) (or ArgumentsNValidator.unwrap(…​)) turns it back:

Arguments1Validator<Arguments1<String>, Name> wrappedValidator = nameValidator.wrap();
Arguments1Validator<String, Name> unwrappedValidator = Arguments1Validator.unwrap(wrappedValidator);

lazy() returns a validator that produces a Supplier of the validated value. The value is created only when the supplier is used, which lets you validate arguments inside a constructor without recursing into the constructor:

Arguments1Validator<String, Supplier<Name>> lazyValidator = nameValidator.lazy();
Supplier<Name> nameSupplier = lazyValidator.validated("John Doe");

See The Always Valid model for the lazy() usage in a record constructor.

indexed(int) appends [index] to violation names so that violations of collection elements can be told apart:

Arguments1Validator<String, Email> indexedEmailValidator = emailValidator.indexed(0);

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 = emailValidator.liftList();
Validated<List<Email>> emailsValidated = emailsValidator.validate(List.of("foo@example.com", "bar@example.com"));

Arguments1Validator<Iterable<String>, Set<Email>> emailSetValidator = emailValidator.liftSet();
Arguments1Validator<Iterable<String>, List<Email>> emailCollectionValidator = emailValidator
    .liftCollection(ArrayList::new);
Arguments1Validator<Optional<String>, Optional<Email>> emailOptionalValidator = emailValidator.liftOptional();

liftList(), liftSet(), liftCollection(Supplier) and liftOptional() are available as well.

split(…​) validates separate arguments with separate validators, and combine(…​) validates the same input with multiple validators. ArgumentsValidators.split(…​) and ArgumentsValidators.combine(…​) are described in Combining validators.

ArgumentsValidators.traverseN(…​) validates each element of a given collection with a validator produced by a function and collects the results. sequenceN(…​) applies multiple validators to the same arguments and collects the results:

Arguments1Validator<String, List<Name>> namesValidator = ArgumentsValidators
    .traverse1(List.of("John", "Jane"), value -> nameValidator);

Arguments1Validator<String, List<String>> sequencedValidator = ArgumentsValidators
    .sequence1(List.of(nameValidator.map(Name::value), nameValidator.map(Name::value)));

These "small Validators" can be very powerful parts by using the Validator combinators described in "Combining validators".

5.6. Arguments containers

Arguments1 to Arguments16 are immutable containers of validated arguments. Use Arguments.of(…​) to create one:

Arguments3<String, String, Integer> arguments = Arguments.of("John Doe", "jdoe@example.com", 30);
String name = arguments.arg1();
String email = arguments.arg2();
Integer age = arguments.arg3();

argN() returns the argument at position N. firstN() and lastN() return the leading or trailing N arguments as a new container, and append, prepend and reverse return a modified copy:

Arguments1<String> first = arguments.first1();
Arguments2<String, String> firstTwo = arguments.first2();
Arguments1<Integer> last = arguments.last1();
Arguments2<String, Integer> lastTwo = arguments.last2();

Arguments4<String, String, Integer, String> appended = arguments.append("extra");
Arguments4<String, String, String, Integer> prepended = arguments.prepend("extra");
Arguments3<Integer, String, String> reversed = arguments.reverse();

equals and hashCode are implemented for the contents of the container. Arguments is final, and ArgumentsN does not inherit from ArgumentsM; the concrete container class is part of the equality check.

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.

ArgumentsValidators#split and ArgumentsValidators#combine support up to 16 arguments since 0.10.0. The apply method consumes Function1 to Function16 accordingly.

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. Chaining and mapping value validators

ValueValidator#andThen(ValueValidator) applies the value produced by one validator to the next validator. ValueValidator#map(Function) maps the validated value without applying further validation. andThen(Function) is deprecated since 0.17.0 and is equivalent to map(Function).

Arguments1Validator<String, Email> emailValidator = /* see examples above */;
ValueValidator<String, String> emailValue = emailValidator.map(Email::value);
ValueValidator<String, String> checked = emailValidator
    .andThen(ValueValidator.<Email> passThrough().map(Email::value));

6.3. Validating every element of an iterable

ArgumentsValidators#sequenceN applies every given validator to the same arguments and collects the validated values into a List. ArgumentsValidators#traverseN builds a validator from an iterable of values and a function that produces a validator for each value. Both are available for 1 to 16 arguments since 0.10.0.

Arguments1Validator<String, Email> emailValidator = /* see examples above */;
Arguments1Validator<String, Email> adminEmailValidator = /* see examples above */;
Arguments1Validator<String, List<Email>> emailsValidator = ArgumentsValidators
    .sequence1(List.of(emailValidator, adminEmailValidator));

Validated<List<Email>> emails = emailsValidator.validate("yavi@example.com");

See Validating arguments for constructing arguments validators.

6.4. 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:

Arguments1Validator<HttpServletRequest, Name> requestNameValidator = nameValidator
    .compose(req -> req.getParameter("name"));
Arguments1Validator<HttpServletRequest, Email> requestEmailValidator = emailValidator
    .compose(req -> req.getParameter("email"));
Arguments1Validator<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();

Unless stated otherwise, a null value is valid and the check is skipped in the following constraints.

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

object.notNull

"{0}" must not be null

  • {0} …​ field name

  • {1} …​ the given value

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

object.isNull

"{0}" must be null

  • {0} …​ field name

  • {1} …​ the given value

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

container.notEmpty

"{0}" must not be empty

  • {0} …​ field name

  • {1} …​ the given value

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

charSequence.notBlank

"{0}" must not be blank

  • {0} …​ field name

  • {1} …​ the given value

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

container.fixedSize

The size of "{0}" must be {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

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

container.greaterThan

The size of "{0}" must be greater than {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

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

container.greaterThanOrEqual

The size of "{0}" must be greater than or equal to {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

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

container.lessThan

The size of "{0}" must be less than {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

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 4. The given size is 5)
Message Key Default Message Format Args

container.lessThanOrEqual

The size of "{0}" must be less than or equal to {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

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

charSequence.contains

"{0}" must contain {1}

  • {0} …​ field name

  • {1} …​ the specified char sequence

  • {2} …​ the given value

7.1.11. startsWith(CharSequence)

Checks if the target value starts with the specified prefix

Validator<Code> validator = ValidatorBuilder.<Code> of()
    .constraint(Code::asString, "code", c -> c.startsWith("JP-"))
    .build();

ConstraintViolations violations = validator.validate(new Code("JP-1234")); // Valid
ConstraintViolations violations = validator.validate(new Code(null)); // Valid
ConstraintViolations violations = validator.validate(new Code("US-1234")); // Invalid ("code" must start with "JP-")
Message Key Default Message Format Args

charSequence.startsWith

"{0}" must start with "{1}"

  • {0} …​ field name

  • {1} …​ the specified prefix

  • {2} …​ the given value

7.1.12. endsWith(CharSequence)

Checks if the target value ends with the specified suffix

Validator<Domain> validator = ValidatorBuilder.<Domain> of()
    .constraint(Domain::asString, "domain", c -> c.endsWith(".example.com"))
    .build();

ConstraintViolations violations = validator.validate(new Domain("www.example.com")); // Valid
ConstraintViolations violations = validator.validate(new Domain(null)); // Valid
ConstraintViolations violations = validator.validate(new Domain("example.com")); // Invalid ("domain" must end with ".example.com")
Message Key Default Message Format Args

charSequence.endsWith

"{0}" must end with "{1}"

  • {0} …​ field name

  • {1} …​ the specified suffix

  • {2} …​ the given value

7.1.13. 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

charSequence.pattern

"{0}" must match {1}

  • {0} …​ field name

  • {1} …​ the specified regular expression

  • {2} …​ the given value

7.1.14. pattern(Pattern) / pattern(Supplier<Pattern>)

Checks if the target value matches the specified regular expression, in the same way as pattern(String). The Supplier<Pattern> overload invokes the supplier for every validation, which is intended for regular expressions that are built dynamically at validation time.

Pattern zipCode = Pattern.compile("[0-9]{3}-[0-9]{4}");
Validator<ZipCode> validator = ValidatorBuilder.<ZipCode> of()
    .constraint(ZipCode::asString, "zipCode", c -> c.pattern(zipCode))
    .build();

Validator<ZipCode> dynamicValidator = ValidatorBuilder.<ZipCode> of()
    .constraint(ZipCode::asString, "zipCode", c -> c.pattern(() -> Pattern.compile("[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

charSequence.pattern

"{0}" must match {1}

  • {0} …​ field name

  • {1} …​ the specified regular expression

  • {2} …​ the given value

7.1.15. 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 be a valid email address)
Message Key Default Message Format Args

charSequence.email

"{0}" must be a valid email address

  • {0} …​ field name

  • {1} …​ the given value

7.1.16. uuid()

Checks if the target value is a valid UUID in the 8-4-4-4-12 hexadecimal format. The version and the variant are not checked, and an empty value is valid.

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraint(User::getId, "id", c -> c.uuid())
    .build();

ConstraintViolations violations = validator.validate(new User("123e4567-e89b-12d3-a456-426614174000")); // Valid
ConstraintViolations violations = validator.validate(new User(null)); // Valid
ConstraintViolations violations = validator.validate(new User("")); // Valid
ConstraintViolations violations = validator.validate(new User("1234")); // Invalid ("id" must be a valid UUID)
Message Key Default Message Format Args

charSequence.uuid

"{0}" must be a valid UUID

  • {0} …​ field name

  • {1} …​ the given value

7.1.17. 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

password.required

"{0}" must meet {1} policy

  • {0} …​ field name

  • {1} …​ the specified policy name

  • {2} …​ the given value

password.optional

"{0}" must meet at least {1} policies from {2}

  • {0} …​ field name

  • {1} …​ minimum requirement

  • {2} …​ the specified policy names

  • {3} …​ the given value

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.18. 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 be a valid IPv4)
Message Key Default Message Format Args

charSequence.ipv4

"{0}" must be a valid IPv4

  • {0} …​ field name

  • {1} …​ the given value

7.1.19. 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 be a valid IPv6)
Message Key Default Message Format Args

charSequence.ipv6

"{0}" must be a valid IPv6

  • {0} …​ field name

  • {1} …​ the given value

7.1.20. url()

Check if the target value is a valid URL. An empty value is valid.

Since 0.17.0 the value is parsed as a java.net.URI instead of the deprecated URL(String) constructor, which silently strips leading and trailing characters. Values that only passed because of that trimming, values that contain unencoded characters, and values with an empty authority are now rejected.
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)
ConstraintViolations violations = validator.validate(new Url(" https://yavi.ik.am ")); // Invalid ("url" must be a valid URL)
ConstraintViolations violations = validator.validate(new Url("https://yavi.ik.am/path with space")); // Invalid ("url" must be a valid URL)
ConstraintViolations violations = validator.validate(new Url("https://")); // Invalid ("url" must be a valid URL)
Message Key Default Message Format Args

charSequence.url

"{0}" must be a valid URL

  • {0} …​ field name

  • {1} …​ the given value

7.1.21. url(…​)

Check if the target value is a valid URL and restrict its protocol, host and/or port. The value is first required to be a valid URL, and an unexpected attribute is reported in addition to any URL violation. This is the equivalent of Bean Validation’s @URL(protocol = "https", host = "example.com", port = 8443). An empty value is valid.

Hosts are compared case-insensitively but matched as written, so host("example.com") does not accept https://www.example.com. A URL without an explicit port is matched against the default port of its protocol, so port(443) accepts https://example.com. If the value cannot be parsed as a URL, only charSequence.url is reported.

Validator<Url> validator = ValidatorBuilder.<Url> of()
    .constraint(Url::asString, "url", c -> c.url(url -> url
            .protocol("https")
            .host("example.com")
            .port(8443)
            .build()))
    .build();

ConstraintViolations violations = validator.validate(new Url("https://example.com:8443/app")); // Valid
ConstraintViolations violations = validator.validate(new Url("http://example.com:8443/app")); // Invalid (The protocol of "url" must be one of the following values: [https])
ConstraintViolations violations = validator.validate(new Url("https://other.example.com:8443/app")); // Invalid (The host of "url" must be one of the following values: [example.com])
ConstraintViolations violations = validator.validate(new Url("https://example.com:9999/app")); // Invalid (The port of "url" must be one of the following values: [8443])
Message Key Default Message Format Args

charSequence.url

"{0}" must be a valid URL

  • {0} …​ field name

  • {1} …​ the given value

charSequence.url.protocol

The protocol of "{0}" must be one of the following values: {1}

  • {0} …​ field name

  • {1} …​ the specified protocols

  • {2} …​ the given value

charSequence.url.host

The host of "{0}" must be one of the following values: {1}

  • {0} …​ field name

  • {1} …​ the specified hosts

  • {2} …​ the given value

charSequence.url.port

The port of "{0}" must be one of the following values: {1}

  • {0} …​ field name

  • {1} …​ the specified ports

  • {2} …​ the given value

7.1.22. 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

charSequence.luhn

the check digit for "{0}" is invalid, Luhn checksum failed

  • {0} …​ field name

  • {1} …​ the given value

7.1.23. 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

charSequence.byte

"{0}" must be a valid representation of a byte

  • {0} …​ field name

  • {1} …​ the given value

7.1.24. 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

charSequence.short

"{0}" must be a valid representation of a short

  • {0} …​ field name

  • {1} …​ the given value

7.1.25. 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

charSequence.integer

"{0}" must be a valid representation of an integer

  • {0} …​ field name

  • {1} …​ the given value

7.1.26. 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

charSequence.long

"{0}" must be a valid representation of a long

  • {0} …​ field name

  • {1} …​ the given value

7.1.27. 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

charSequence.float

"{0}" must be a valid representation of a float.

  • {0} …​ field name

  • {1} …​ the given value

7.1.28. 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

charSequence.double

"{0}" must be a valid representation of a double

  • {0} …​ field name

  • {1} …​ the given value

7.1.29. 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

charSequence.biginteger

"{0}" must be a valid representation of a big integer

  • {0} …​ field name

  • {1} …​ the given value

7.1.30. 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

charSequence.bigdecimal

"{0}" must be a valid representation of a big decimal

  • {0} …​ field name

  • {1} …​ the given value

7.1.31. isoLocalDate() / localDate(String pattern)

Checks if the target value is a valid local date in the given pattern, resolved with ResolverStyle.STRICT. isoLocalDate() uses the pattern uuuu-MM-dd. The formatter is created each time a value is validated. An empty value is valid.

Validator<BirthDate> validator = ValidatorBuilder.<BirthDate> of()
    .constraint(BirthDate::asString, "birthDate", c -> c.isoLocalDate())
    .build();
Validator<BirthDate> customValidator = ValidatorBuilder.<BirthDate> of()
    .constraint(BirthDate::asString, "birthDate", c -> c.localDate("dd/MM/uuuu"))
    .build();

ConstraintViolations violations = validator.validate(new BirthDate("2024-12-31")); // Valid
ConstraintViolations violations = validator.validate(new BirthDate(null)); // Valid
ConstraintViolations violations = validator.validate(new BirthDate("2024-02-30")); // Invalid ("birthDate" must be a valid representation of a local date using the pattern: uuuu-MM-dd. The give value is: 2024-02-30)
ConstraintViolations violations = customValidator.validate(new BirthDate("31/12/2024")); // Valid
Message Key Default Message Format Args

charSequence.localdate

"{0}" must be a valid representation of a local date using the pattern: {1}. The give value is: {2}

  • {0} …​ field name

  • {1} …​ the specified date pattern

  • {2} …​ the given value

7.1.32. 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

codePoints.asWhiteList

"{1}" is/are not allowed for "{0}"

  • {0} …​ field name

  • {1} …​ the violated value

codePoints.asBlackList

"{1}" is/are not allowed for "{0}"

  • {0} …​ field name

  • {1} …​ the violated value

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) and 0x000D (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.33. 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 violations = 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 violations = 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 violations = 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 violations = 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 violations = validator.validate(new Message("❤️")); // Valid
violations = validator.validate(new Message("🤴🏻")); // Valid
violations = validator.validate(new Message("👨‍👦")); // Valid
violations = validator.validate(new Message("️👨‍👨‍👧‍👦")); // Valid
violations = validator.validate(new Message("🏴󠁧󠁢󠁥󠁮󠁧󠁿")); // Valid

7.1.34. variant(…​)

Returns a constraint that removes the selected variation selectors before the size is measured. This does not change the value itself; it only affects how the size is counted. See Advanced character length check for the details of IVS, SVS and FVS.

Validator<Message> validator = ValidatorBuilder.<Message> of()
    .constraint(Message::getText, "text", c -> c.variant(opts -> opts.fvs(MongolianFreeVariationSelector.IGNORE))
            .fixedSize(1))
    .build();

ConstraintViolations violations = validator.validate(new Message("ᠠ᠋")); // Valid

variant(…​) is not a constraint itself and has no message of its own; the message of the size constraint applied to its result is reported.

7.1.35. 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 violations = validator.validate(new Message("I❤️☕️️")); // Valid
violations = 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

byteSize.lessThan

The byte size of "{0}" must be less than {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

byteSize.lessThanOrEqual

The byte size of "{0}" must be less than or equal to {1}. The given size is {2}"

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

byteSize.greaterThan

The byte size of "{0}" must be greater than {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

byteSize.greaterThanOrEqual

The byte size of "{0}" must be greater than or equal to {1}. The given size is {2}"

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

byteSize.fixedSize

The byte size of "{0}" must be {1}. The given size is {2}"

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

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

object.notNull

"{0}" must not be null

  • {0} …​ field name

  • {1} …​ the given value

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

object.isNull

"{0}" must be null

  • {0} …​ field name

  • {1} …​ the given value

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

numeric.greaterThan

"{0}" must be greater than {1}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given value

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 20)
Message Key Default Message Format Args

numeric.greaterThanOrEqual

"{0}" must be greater than or equal to {1}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given value

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

numeric.lessThan

"{0}" must be less than {1}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given value

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 20)
Message Key Default Message Format Args

numeric.lessThanOrEqual

"{0}" must be less than or equal to {1}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given value

7.2.7. positive() / positiveOrZero() / negative() / negativeOrZero()

Checks if the target value is positive, positive or zero, negative, or negative or zero, respectively. These constraints apply to Byte, Short, Integer, Long, Float, Double, BigInteger and BigDecimal.

Validator<Age> validator = ValidatorBuilder.<Age> of()
    .constraint(Age::asInt, "age", c -> c.positive())
    .build();

ConstraintViolations violations = validator.validate(new Age(1)); // Valid
ConstraintViolations violations = validator.validate(new Age(null)); // Valid
ConstraintViolations violations = validator.validate(new Age(0)); // Invalid ("age" must be positive)

positiveOrZero, negative and negativeOrZero are used in the same way.

Message Key Default Message Format Args

numeric.positive

"{0}" must be positive

  • {0} …​ field name

  • {1} …​ the given value

numeric.positiveOrZero

"{0}" must be positive or zero

  • {0} …​ field name

  • {1} …​ the given value

numeric.negative

"{0}" must be negative

  • {0} …​ field name

  • {1} …​ the given value

numeric.negativeOrZero

"{0}" must be negative or zero

  • {0} …​ field name

  • {1} …​ the given value

7.3. Temporal

YAVI provides temporal constraints for the following types, with the corresponding builder shortcuts:

Type Constraint Builder shortcut

LocalDate

am.ik.yavi.constraint.LocalDateConstraint

_localDate

LocalTime

am.ik.yavi.constraint.LocalTimeConstraint

_localTime

LocalDateTime

am.ik.yavi.constraint.LocalDateTimeConstraint

_localDateTime

OffsetDateTime

am.ik.yavi.constraint.OffsetDateTimeConstraint

_offsetDateTime

Instant

am.ik.yavi.constraint.InstantConstraint

_instant

Year

am.ik.yavi.constraint.YearConstraint

_year

YearMonth

am.ik.yavi.constraint.YearMonthConstraint

_yearMonth

ZonedDateTime

am.ik.yavi.constraint.ZonedDateTimeConstraint

_zonedDateTime

Custom TemporalAccessor types can be supported by extending TemporalConstraintBase through ChronoLocalDateConstraintBase, ChronoLocalDateTimeConstraintBase or ChronoZonedDateTimeConstraintBase. The Kotlin DSL provides matching property overloads (see the Kotlin Support section).

The following constraints apply to every type above:

Validator<Event> validator = ValidatorBuilder.<Event> of()
    ._localDate(Event::getDate, "date", c -> c.past())
    .build();

A null value is valid and skips all temporal checks.

before and after reject a value that is equal to the boundary; use beforeOrEqual and afterOrEqual for inclusive checks. The arguments are always suppliers.

before, beforeOrEqual, after, afterOrEqual and between have an overload that takes a boolean memoize argument. When memoize is true, which is the default, the supplier is called at most once and the same value is used for both the comparison and the violation message. When it is false, the supplier is called on every use, so a supplier that returns a different value on each call may compare against one value and report another in the message. Use false for suppliers that must be evaluated at validation time.

The past, pastOrPresent, future and futureOrPresent methods also have overloads that take a java.time.Clock. These overloads exist for deterministic tests and for validating against a time source other than the system default zone.

7.3.1. past() / pastOrPresent() / future() / futureOrPresent()

Checks if the target value is in the past, in the past or present, in the future, or in the present or future, respectively. The overloads take a Clock used as the current time.

Clock clock = Clock.fixed(Instant.parse("2024-01-01T00:00:00Z"), ZoneId.of("UTC"));
Validator<Event> validator = ValidatorBuilder.<Event> of()
    ._localDate(Event::getDate, "date", c -> c.past(clock))
    .build();

ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2023, 12, 31))); // Valid
ConstraintViolations violations = validator.validate(new Event(null)); // Valid
ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 1, 1))); // Invalid ("date" must be a past date)
ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 1, 2))); // Invalid ("date" must be a past date)
Message Key Default Message Format Args

temporal.past

"{0}" must be a past date

  • {0} …​ field name

  • {1} …​ the current date used for the comparison

  • {2} …​ the given value

temporal.pastOrPresent

"{0}" must be a date in the past or in the present

  • {0} …​ field name

  • {1} …​ the current date used for the comparison

  • {2} …​ the given value

temporal.future

"{0}" must be a future date

  • {0} …​ field name

  • {1} …​ the current date used for the comparison

  • {2} …​ the given value

temporal.futureOrPresent

"{0}" must be a date in the present or in the future

  • {0} …​ field name

  • {1} …​ the current date used for the comparison

  • {2} …​ the given value

7.3.2. before(…​) / beforeOrEqual(…​) / after(…​) / afterOrEqual(…​)

Checks if the target value is before, before or equal to, after, or after or equal to the temporal supplied by the given supplier.

Validator<Event> validator = ValidatorBuilder.<Event> of()
    ._localDate(Event::getDate, "date", c -> c.before(() -> LocalDate.of(2024, 6, 1)))
    .build();

ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 5, 31))); // Valid
ConstraintViolations violations = validator.validate(new Event(null)); // Valid
ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 6, 1))); // Invalid ("date" has to be before 2024-06-01)
ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 6, 2))); // Invalid ("date" has to be before 2024-06-01)

The beforeOrEqual, after and afterOrEqual variants are used in the same way.

Message Key Default Message Format Args

temporal.before

"{0}" has to be before {1}

  • {0} …​ field name

  • {1} …​ the supplied boundary value

  • {2} …​ the given value

temporal.beforeOrEqual

"{0}" has to be before or equals to {1}

  • {0} …​ field name

  • {1} …​ the supplied boundary value

  • {2} …​ the given value

temporal.after

"{0}" has to be after {1}

  • {0} …​ field name

  • {1} …​ the supplied boundary value

  • {2} …​ the given value

temporal.afterOrEqual

"{0}" has to be after or equals to {1}

  • {0} …​ field name

  • {1} …​ the supplied boundary value

  • {2} …​ the given value

7.3.3. between(…​)

Checks if the target value is between the temporals supplied by the given suppliers. The range is exclusive on both ends: a value equal to either bound is invalid. IllegalArgumentException is thrown at validation time if rangeFrom is after rangeTo.

Validator<Event> validator = ValidatorBuilder.<Event> of()
    ._localDate(Event::getDate, "date", c -> c.between(
            () -> LocalDate.of(2024, 1, 1), () -> LocalDate.of(2024, 12, 31)))
    .build();

ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 6, 1))); // Valid
ConstraintViolations violations = validator.validate(new Event(null)); // Valid
ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 1, 1))); // Invalid ("date" has to be between 2024-01-01 and 2024-12-31)
ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2025, 1, 1))); // Invalid ("date" has to be between 2024-01-01 and 2024-12-31)
Message Key Default Message Format Args

temporal.between

"{0}" has to be between {1} and {2}

  • {0} …​ field name

  • {1} …​ the supplied start of the range

  • {2} …​ the supplied end of the range

  • {3} …​ the given value

7.3.4. fieldPredicate(…​)

Checks if the given predicate accepts the value of the given java.time.temporal.TemporalField.

Validator<Event> validator = ValidatorBuilder.<Event> of()
    ._localDate(Event::getDate, "date", c -> c.fieldPredicate(ChronoField.DAY_OF_WEEK, d -> d >= 1 && d <= 5))
    .build();

ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 6, 3))); // Valid
ConstraintViolations violations = validator.validate(new Event(null)); // Valid
ConstraintViolations violations = validator.validate(new Event(LocalDate.of(2024, 6, 1))); // Invalid (The DayOfWeek of "date" is invalid)
Message Key Default Message Format Args

temporal.field

The {1} of "{0}" is invalid

  • {0} …​ field name

  • {1} …​ the temporal field

  • {2} …​ the given value

7.4. 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.4.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

object.notNull

"{0}" must not be null

  • {0} …​ field name

  • {1} …​ the given value

7.4.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

object.isNull

"{0}" must be null

  • {0} …​ field name

  • {1} …​ the given value

7.4.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

boolean.isTrue

"{0}" must be true

  • {0} …​ field name

  • {1} …​ the given value

7.4.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

boolean.isFalse

"{0}" must be false

  • {0} …​ field name

  • {1} …​ the given value

7.5. 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.5.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

object.notNull

"{0}" must not be null

  • {0} …​ field name

  • {1} …​ the given value

7.5.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

object.isNull

"{0}" must be null

  • {0} …​ field name

  • {1} …​ the given value

7.5.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

container.notEmpty

"{0}" must not be empty

  • {0} …​ field name

  • {1} …​ the given value

7.5.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

container.fixedSize

The size of "{0}" must be {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.5.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

container.greaterThan

The size of "{0}" must be greater than {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.5.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

container.greaterThanOrEqual

The size of "{0}" must be greater than or equal to {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.5.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

container.lessThan

The size of "{0}" must be less than {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.5.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

container.lessThanOrEqual

The size of "{0}" must be less than or equal to {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.5.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

collection.contains

"{0}" must contain {1}

  • {0} …​ field name

  • {1} …​ the specified value

  • {2} …​ the given value

7.5.10. unique()

Checks if the elements of the target value are unique. The violation includes the duplicated elements.

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
    .constraint(Histories::asList, "histories", c -> c.unique())
    .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(1), new History(1)))); // Invalid ("histories" must be unique. [History{revision=1}] is/are duplicated.)
Message Key Default Message Format Args

collection.unique

"{0}" must be unique. {1} is/are duplicated.

  • {0} …​ field name

  • {1} …​ the duplicated elements

  • {2} …​ the given value

7.5.11. containsAll(Collection)

Checks if the target value contains all of the given elements. The violation includes the missing elements. A Set and a non-Set collection are handled with the same semantics.

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
    .constraint(Histories::asList, "histories", c -> c.containsAll(List.of(new History(1), new History(2))))
    .build();

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(1), new History(2), new History(3)))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(1)))); // Invalid ("histories" must be have of all of the following values: [History{revision=1}, History{revision=2}], missing values: [History{revision=2}])
Message Key Default Message Format Args

collection.containsAll

"{0}" must be have of all of the following values: {1}, missing values: {2}

  • {0} …​ field name

  • {1} …​ the required elements

  • {2} …​ the missing elements

  • {3} …​ the given value

7.5.12. forEach(…​) / forEachIfPresent(…​)

forEach applies an element validator to each element of a collection property, and forEachIfPresent does the same but treats a null collection as valid. A null element inside the collection is reported with the element index in the property name, for example histories[1], and the object.notNull message.

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
    .forEach(Histories::asList, "histories",
            ValidatorBuilder.<History> of()
                .constraint(History::getRevision, "revision", c -> c.notNull())
                .build())
    .build();

ConstraintViolations violations = validator.validate(new Histories(Arrays.asList(new History(1), null))); // Invalid ("histories[1]" must not be null)
Message Key Default Message Format Args

object.notNull

"{0}" must not be null

  • {0} …​ the indexed field name

7.6. 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.6.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

object.notNull

"{0}" must not be null

  • {0} …​ field name

  • {1} …​ the given value

7.6.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

object.isNull

"{0}" must be null

  • {0} …​ field name

  • {1} …​ the given value

7.6.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

container.notEmpty

"{0}" must not be empty

  • {0} …​ field name

  • {1} …​ the given value

7.6.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

container.fixedSize

The size of "{0}" must be {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.6.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

container.greaterThan

The size of "{0}" must be greater than {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.6.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

container.greaterThanOrEqual

The size of "{0}" must be greater than or equal to {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.6.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

container.lessThan

The size of "{0}" must be less than {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.6.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

container.lessThanOrEqual

The size of "{0}" must be less than or equal to {1}. The given size is {2}

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given size

  • {3} …​ the given value

7.6.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

map.containsKey

"{0}" must contain key {1}

  • {0} …​ field name

  • {1} …​ the specified key

  • {2} …​ the given value

7.6.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

map.containsValue

"{0}" must contain value {1}

  • {0} …​ field name

  • {1} …​ the specified value

  • {2} …​ the given value

7.7. 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-field 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.7.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

object.notNull

"{0}" must not be null

  • {0} …​ field name

  • {1} …​ the given value

7.7.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

object.isNull

"{0}" must be null

  • {0} …​ field name

  • {1} …​ the given value

7.7.3. equalTo(Object)

Checks if the target value is equal to the specified value

Validator<Box> validator = ValidatorBuilder.<Box> of()
    ._object(Box::getValue, "value", c -> c.equalTo("expected"))
    .build();

ConstraintViolations violations = validator.validate(new Box("expected")); // Valid
ConstraintViolations violations = validator.validate(new Box(null)); // Valid
ConstraintViolations violations = validator.validate(new Box("other")); // Invalid ("value" must be equal to expected)

7.7.4. oneOf(Collection)

Checks if the target value is one of the specified values

Validator<Box> validator = ValidatorBuilder.<Box> of()
    ._object(Box::getValue, "value", c -> c.oneOf(List.of("a", "b")))
    .build();

ConstraintViolations violations = validator.validate(new Box("a")); // Valid
ConstraintViolations violations = validator.validate(new Box(null)); // Valid
ConstraintViolations violations = validator.validate(new Box("c")); // Invalid ("value" must be one of the following values: [a, b])

7.7.5. notOneOf(Collection)

Checks if the target value is not one of the specified values

Validator<Box> validator = ValidatorBuilder.<Box> of()
    ._object(Box::getValue, "value", c -> c.notOneOf(List.of("a", "b")))
    .build();

ConstraintViolations violations = validator.validate(new Box("c")); // Valid
ConstraintViolations violations = validator.validate(new Box(null)); // Valid
ConstraintViolations violations = validator.validate(new Box("a")); // Invalid ("value" must not be one of the following values: [a, b])
Message Key Default Message Format Args

object.equalTo

"{0}" must be equal to {1}

  • {0} …​ field name

  • {1} …​ the specified value

  • {2} …​ the given value

object.oneOf

"{0}" must be one of the following values: {1}

  • {0} …​ field name

  • {1} …​ the allowed values

  • {2} …​ the given value

object.notOneOf

"{0}" must not be one of the following values: {1}

  • {0} …​ field name

  • {1} …​ the forbidden values

  • {2} …​ the given value

7.7.6. 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

password.required

"{0}" must meet {1} policy

  • {0} …​ field name

  • {1} …​ the specified policy name

  • {2} …​ the given value

password.optional

"{0}" must meet at least {1} policies from {2}

  • {0} …​ field name

  • {1} …​ minimum requirement

  • {2} …​ the specified policy names

  • {3} …​ the given value

7.8. Enum

Apply constraints on an enum as follows:

Validator<Account> validator = ValidatorBuilder.<Account> of()
    ._enum(Account::getStatus, "status", c -> c.oneOf(Status.ACTIVE))
    .build();

ConstraintViolations violations = validator.validate(new Account(Status.ACTIVE)); // Valid
ConstraintViolations violations = validator.validate(new Account(null)); // Valid
ConstraintViolations violations = validator.validate(new Account(Status.INACTIVE)); // Invalid ("status" must be one of the following values: [ACTIVE])

_enum is an alias of constraint(ToEnum, String, Function). The same _enum method is available on ArgumentsValidatorBuilder and YaviArguments to validate an enum argument.

EnumConstraint#oneOf(E…​) requires the value to be one of the given enum constants. An empty argument list is rejected with IllegalArgumentException. A null value is valid unless notNull() is added. The violation uses the object.oneOf message key.

A single enum value can be validated with EnumValidatorBuilder and EnumValidator as follows:

EnumValidator<Status, Status> validator = EnumValidatorBuilder.<Status> of("status",
        c -> c.oneOf(Status.ACTIVE)).build();

Validated<Status> result = validator.validate(Status.INACTIVE); // Invalid ("status" must be one of the following values: [ACTIVE])
Message Key Default Message Format Args

object.oneOf

"{0}" must be one of the following values: {1}

  • {0} …​ field name

  • {1} …​ the allowed values

  • {2} …​ the given value

7.9. Array

Apply the constraints on arrays as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    ._objectArray(/* Function from TargetClass to an object array  */, /* Field Name */, /* Constraints */)
    .build();

The following constraints and builder shortcuts are available:

Array type Builder shortcut

E[] (ObjectArrayConstraint)

_objectArray

boolean[] (BooleanArrayConstraint)

_booleanArray

byte[] (ByteArrayConstraint)

_byteArray

char[] (CharArrayConstraint)

_charArray

short[] (ShortArrayConstraint)

_shortArray

int[] (IntArrayConstraint)

_intArray

long[] (LongArrayConstraint)

_longArray

float[] (FloatArrayConstraint)

_floatArray

double[] (DoubleArrayConstraint)

_doubleArray

The constraints in am.ik.yavi.constraint.array provide notNull, isNull, notEmpty, fixedSize, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual and contains. A null array is valid and the checks are skipped, except for notEmpty().

Validator<Values> validator = ValidatorBuilder.<Values> of()
    ._intArray(Values::getNumbers, "numbers", c -> c.contains(3).fixedSize(3))
    .build();

ConstraintViolations violations = validator.validate(new Values(new int[] { 1, 2, 3 })); // Valid
ConstraintViolations violations = validator.validate(new Values(null)); // Valid
ConstraintViolations violations = validator.validate(new Values(new int[] { 1, 2 })); // Invalid ("numbers" must contain 3, The size of "numbers" must be 3. The given size is 2)

The size and null checks report the object.notNull, object.isNull and container.* message keys documented in the Collection section. contains reports array.contains.

Message Key Default Message Format Args

array.contains

"{0}" must contain {1}

  • {0} …​ field name

  • {1} …​ the specified value

  • {2} …​ the given value

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.

8.1. Enabling the annotation processor

The annotation processor is packaged in the yavi artifact. With JDK 21 and earlier, mvn clean compile discovers it on the compile classpath automatically. JDK 23 and later no longer run annotation processors found on the classpath implicitly, so declare the processor explicitly in pom.xml:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>am.ik.yavi</groupId>
                <artifactId>yavi</artifactId>
                <version>0.17.3</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

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.

@ConstraintTarget and @ConstraintArguments are compile-time annotations, so they disappear at runtime. + This won’t break YAVI’s "No annotation!" concept ;)

8.2. 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.notBlank())
    .constraint(_UserFormMeta.EMAIL, c -> c.notBlank().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.2.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 2026-11-12T12:52:16.335171+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.2.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 2026-11-12T12:52:16.335171+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.2.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 2026-11-12T12:52:16.333598+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.2.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 2026-11-12T12:52:16.331344+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.3. 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 @ConstraintArguments will generate _XyzArgumentsMeta.

doSomething method of Xyz class that is annotated with @ConstraintArguments will generate _XyzDoSomethingArgumentsMeta.

8.3.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 2026-11-12T12:52:16.325765+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.Arguments3::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.Arguments3::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.notBlank())
        .constraint(_UserArgumentsMeta.EMAIL, c -> c.notBlank().email())
        .constraint(_UserArgumentsMeta.AGE, c -> c.greaterThan(0))
    )
    .build();

8.3.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 2026-11-12T12:52:16.329116+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.Arguments4::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.Arguments4::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.Arguments4::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.notBlank())
        .constraint(_UserServiceCreateUserArgumentsMeta.EMAIL, c -> c.notBlank().email())
        .constraint(_UserServiceCreateUserArgumentsMeta.AGE, c -> c.greaterThan(0))
    )
    .build();

8.4. Supported meta types

The processor generates a meta for the declared type of the annotated target. The following meta types are available in am.ik.yavi.meta:

  • StringConstraintMeta

  • BooleanConstraintMeta, CharacterConstraintMeta, ByteConstraintMeta, ShortConstraintMeta, IntegerConstraintMeta, LongConstraintMeta, FloatConstraintMeta, DoubleConstraintMeta

  • BigIntegerConstraintMeta, BigDecimalConstraintMeta

  • ObjectConstraintMeta

  • LocalDateConstraintMeta, LocalTimeConstraintMeta, LocalDateTimeConstraintMeta, OffsetDateTimeConstraintMeta, InstantConstraintMeta, YearConstraintMeta, YearMonthConstraintMeta, ZonedDateTimeConstraintMeta

There is no dedicated meta type for enums; an enum target uses ObjectConstraintMeta.

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.

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 BiConsumer

Since 0.16.0, a validator is exposed as a java.util.function.BiConsumer<T, E> instead of a dedicated type, where T is the type of the target object and E is the type of the errors object.

This is helpful for libraries or apps to adapt both YAVI and other validation frameworks that accept these two arguments, like Spring Framework’s org.springframework.validation.Validator#validate(Object, Errors).

Validatable#toBiConsumer and ValueValidator#toBiConsumer take an am.ik.yavi.core.ErrorHandler<E>:

public interface ErrorHandler<E> {
    void handleError(E errors, String name, String messageKey, Object[] args, String defaultMessage);
}

For Spring Framework’s Errors, the handler can record the violation with Errors#rejectValue:

Validator<CartItem> cartItemValidator = ...;
BiConsumer<CartItem, Errors> validator = cartItemValidator
    .toBiConsumer((errors, name, messageKey, args, defaultMessage) ->
        errors.rejectValue(name, messageKey, args, defaultMessage));
// or
BiConsumer<CartItem, Errors> validator = cartItemValidator.toBiConsumer(Errors::rejectValue);

There is a factory for BiConsumer as well:

@Bean
public BiConsumerFactory<Errors> biConsumerFactory() {
    return new BiConsumerFactory<>(Errors::rejectValue);
}

or, if you want to customize the message configuration

@Bean
public BiConsumerFactory<Errors> biConsumerFactory(MessageSource messageSource) {
    MessageFormatter messageFormatter = new MessageSourceMessageFormatter(messageSource::getMessage);
    return new BiConsumerFactory<>("." /* Message Key Separator */, messageFormatter, Errors::rejectValue);
}

The usage of a BiConsumer would look like following:

@RestController
public class OrderController {
    private final BiConsumer<CartItem, Errors> validator;

    public OrderController(BiConsumerFactory<Errors> factory) {
        this.validator = factory.validator(builder -> builder.constraint(...));
    }
}