YAVI (pronounced jɑ-vάɪ) is a lambda based type safe validation for Java.

1. Introduction

YAVI sounds as same as a Japanese slang "YABAI (ヤバイ)" that means awesome or awful depending on the context (like "Crazy"). If you use YAVI, you will surely understand that it means the former.

The concepts are

  • No reflection!

  • No (runtime) annotation!

  • Not only Java Beans!

  • Zero dependency!

If you are not a fan of Bean Validation, YAVI will be an awesome alternative.

YAVI has the following features:

  • Type-safe constraints, unsupported constraints cannot be applied to the wrong type

  • Fluent and intuitive API

  • Constraints on any object. Java Beans, Records, Protocol Buffers, Immutables and anything else.

  • Lots of powerful built-in constraints

  • Easy custom constraints

  • Validation for groups, conditional validation

  • Validation for arguments before creating an object

  • Support for API and combination of validation results and validators that incorporate the concept of functional programming

For the migration from Bean Validation, refer the guide.

2. Getting Started

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.9.1</version>
</dependency>

This tutorial uses JUnit 5 and AssertJ. Add the following dependencies as needed:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.21.0</version>
    <scope>test</scope>
</dependency>

2.3. Applying constraints

Let’s dive into an example to see how to apply constraints:

Create src/main/java/com/example/Car.java and write the following code.

package com.example;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;

public class Car {
    private final String manufacturer;

    private final String licensePlate;

    private final int seatCount;

    public static final Validator<Car> validator = ValidatorBuilder.<Car>of()
            .constraint(Car::getManufacturer, "manufacturer", c -> c.notNull())
            .constraint(Car::getLicensePlate, "licensePlate", c -> c.notNull().greaterThanOrEqual(2).lessThanOrEqual(14))
            .constraint(Car::getSeatCount, "seatCount", c -> c.greaterThanOrEqual(2))
            .build();

    public Car(String manufacturer, String licencePlate, int seatCount) {
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    public String getManufacturer() {
        return manufacturer;
    }

    public String getLicensePlate() {
        return licensePlate;
    }

    public int getSeatCount() {
        return seatCount;
    }
}

The ValidatorBuilder#constraint is used to declare the constraints which should be applied to the return values of getter for the Car instance:

  • manufacturer must never be null

  • licensePlate must never be null and must be between 2 and 14 characters long

  • seatCount must be at least 2

You can find the complete source code on GitHub.

2.4. Validating constraints

To perform a validation of these constraints, you use a Validator instance. To demonstrate this, let’s have a look at a simple unit test:

Create src/test/java/com/example/CarTest.java and write the following code.

package com.example;

import am.ik.yavi.core.ConstraintViolations;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CarTest {

    @Test
    void manufacturerIsNull() {
        final Car car = new Car(null, "DD-AB-123", 4);
        final ConstraintViolations violations = Car.validator.validate(car);

        assertThat(violations.isValid()).isFalse();
        assertThat(violations).hasSize(1);
        assertThat(violations.get(0).message()).isEqualTo("\"manufacturer\" must not be null");
    }

    @Test
    void licensePlateTooShort() {
        final Car car = new Car("Morris", "D", 4);
        final ConstraintViolations violations = Car.validator.validate(car);

        assertThat(violations.isValid()).isFalse();
        assertThat(violations).hasSize(1);
        assertThat(violations.get(0).message()).isEqualTo("The size of \"licensePlate\" must be greater than or equal to 2. The given size is 1");
    }

    @Test
    void seatCountTooLow() {
        final Car car = new Car("Morris", "DD-AB-123", 1);
        final ConstraintViolations violations = Car.validator.validate(car);

        assertThat(violations.isValid()).isFalse();
        assertThat(violations).hasSize(1);
        assertThat(violations.get(0).message()).isEqualTo("\"seatCount\" must be greater than or equal to 2");
    }

    @Test
    void carIsValid() {
        final Car car = new Car("Morris", "DD-AB-123", 2);
        final ConstraintViolations violations = Car.validator.validate(car);

        assertThat(violations.isValid()).isTrue();
        assertThat(violations).hasSize(0);
    }
}

Validator instances are thread-safe and may be reused multiple times.

The validate() method returns a ConstraintViolations instance, which you can iterate in order to see which validation errors occurred. The first three test methods show some expected constraint violations:

  • The notNull() constraint 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.

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 interface:

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraintOnCondition((user, constraintGroup) -> !user.getName().isEmpty(),
        b -> b.constraint(User::getEmail, "email", c -> c.email().notEmpty()))
    .build();

The constraint above on email will only be activated if the name is not empty.

3.5. Applying constraints only to specific groups

You can apply constraints only to specific groups with am.ik.yavi.core.ConstraintGroup as a part of ConstraintCondition as well:

enum Group implements ConstraintGroup {
    CREATE, UPDATE, DELETE
}

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraintOnCondition(Group.CREATE.toCondition(), b -> b.constraint(User::getId, "id", c -> c.isNull()))
    .constraintOnCondition(Group.UPDATE.toCondition(), b -> b.constraint(User::getId, "id", c -> c.notNull()))
    .build();

The group to validate can be specified in validate method:

ConstraintViolations violations = validator.validate(user, Group.CREATE);

You can use a shortcut constraintOnGroup method

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraintOnGroup(Group.CREATE, b -> b.constraint(User::getId, "id", c -> c.isNull()))
    .constraintOnGroup(Group.UPDATE, b -> b.constraint(User::getId, "id", c -> c.notNull()))
    .build();

Note that all constraints without conditions will be validated for any constraint group. Also, if no group is specified in the validate method, it will be treated as DEFAULT group.

3.6. Creating a custom constraint

If you want to apply constraints that are not in the "Built-in Constraints", you can create custom constraints by implementing am.ik.yavi.core.CustomConstraint interface as bellow:

public class IsbnConstraint implements CustomConstraint<String> {

    @Override
    public boolean test(String s) {
        // Delegate processing to another method
        return ISBNValidator.isISBN13(s);
    }

    @Override
    public String messageKey() {
        return "string.isbn13";
    }

    @Override
    public String defaultMessageFormat() {
        return "\"{0}\" must be ISBN13 format";
    }
}

The created custom constraint can be specified by predicate method as follows:

IsbnConstraint isbn = new IsbnConstraint();
Validator<Book> book = ValidatorBuilder.<Book> of()
    .constraint(Book::getTitle, "title", c -> c.notBlank().lessThanOrEqual(64))
    .constraint(Book::getIsbn, "isbn", c -> c.notBlank().predicate(isbn))
    .build();

You can also write constraint rules directly in the predicate method instead of defining the CustomConstraint class.

Validator<Book> book = ValidatorBuilder.<Book> of()
    .constraint(Book::getTitle, "title", c -> c.notBlank().lessThanOrEqual(64))
    .constraint(Book::getIsbn, "isbn", c -> c.notBlank()
        .predicate(s -> ISBNValidator.isISBN13(s), "string.isbn13", "\"{0}\" must be ISBN13 format"))
    .build();

The first argument of the violation message is the field name. Also, the last argument is the violated value.

If you want to use other arguments, override arguments method as bellow:

public class InstantRangeConstraint implements CustomConstraint<Instant> {

    private final Instant end;

    private final Instant start;

    InstantRangeConstraint(Instant start, Instant end) {
        this.start = Objects.requireNonNull(start);
        this.end = Objects.requireNonNull(end);
    }

    @Override
    public Object[] arguments(Instant violatedValue) {
        return new Object[] { this.start /* {1} */, this.end /* {2} */};
    }

    @Override
    public String defaultMessageFormat() {
        return "Instant value \"{0}\" must be between \"{1}\" and \"{2}\".";
    }

    @Override
    public String messageKey() {
        return "instant.range";
    }

    @Override
    public boolean test(Instant instant) {
        return instant.isAfter(this.start) && instant.isBefore(this.end);
    }
}

3.7. Cross-field validation

If you want to apply constraints on target class itself, you can use constraintOnTarget. It can be used when you want to apply cross-field constraints as follows:

Validator<Range> validator = ValidatorBuilder.<Range> of()
    .constraint(range::getFrom, "from", c -> c.greaterThan(0))
    .constraint(range::getTo, "to", c -> c.greaterThan(0))
    .constraintOnTarget(range -> range.getTo() > range.getFrom(), "to", "to.isGreaterThanFrom", "\"to\" must be greater than \"from\"")
    .build();

You can also create a custom constraint for the cross-field validation as follows:

public class RangeConstraint implements CustomConstraint<Range> {
    @Override
    public String defaultMessageFormat() {
        return "\"to\" must be greater than \"from\"";
    }

    @Override
    public String messageKey() {
        return "to.isGreaterThanFrom";
    }

    @Override
    public boolean test(Range range) {
        return range.getTo() > range.getFrom();
    }
}

RangeConstraint range = new RangeConstraint();
Validator<Range> validator = ValidatorBuilder.<Range> of()
        .constraintOnTarget(range, "to")
        .build();

3.8. Overriding violation messages

The default violation message for each constraint is defined in "Built-in Constraints".

If you want to customize the violation message, append message method on the target constraint as follows:

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraint(User::getName, "name", c -> c.notNull().message("{0} is required!")
        .greaterThanOrEqual(1).message("{0} is too small!")
        .lessThanOrEqual(20).message("{0} is too large!"))
    .build()

3.9. Message Formatter

YAVI provides am.ik.yavi.message.MessageFormatter interface for constructing violation messages.

By default, am.ik.yavi.message.SimpleMessageFormatter is used, which simply uses java.text.MessageFormatter to interpolate the message. A list of message keys and default message formats is given in "Built-in Constraints".

As a feature of error messages, the following are supported compared to Bean Validation:

  • 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.10. Fail fast mode

Using the fail fast mode, YAVI allows to return from the current validation as soon as the first constraint violation occurs. This can be useful for the validation of large object graphs where you are only interested in a quick check whether there is any constraint violation at all.

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraint(User::getName, "name", c -> c.notNull().lessThanOrEqual(20))
    .constraint(User::getEmail, "email", c -> c.notNull().greaterThanOrEqual(5).lessThanOrEqual(50).email())
    .constraint(User::getAge, "age", c -> c.notNull().greaterThanOrEqual(0).lessThanOrEqual(200))
    .failFast(true) // <-- Enable the fail fast mode
    .build();

You can switch an existing Validator to the fail fast mode as follows:

Validator<User> failFastValidator = validator.failFast(true);

3.11. Kotlin Support

If you are using Kotlin, you can define a Validator with DSL as follows:

val validator = validator<User> {
    User::name {
        notNull()
        lessThanOrEqual(20)
    }
    User::email {
        notNull()
        greaterThanOrEqual(5)
        lessThanOrEqual(50)
        email()
    }
    User::age {
        notNull()
        greaterThanOrEqual(0)
        lessThanOrEqual(100)
    }
}

Field names can be overridden like bellow:

val validator = validator<User> {
    (User::name)("Name") {
        notNull()
        lessThanOrEqual(20)
    }
    (User::email)("Email") {
        notNull()
        greaterThanOrEqual(5)
        lessThanOrEqual(50)
        email()
    }
    (User::age)("Age") {
        notNull()
        greaterThanOrEqual(0)
        lessThanOrEqual(100)
    }
}

forEach example:

val validator = validator<DemoForEachCollection> {
    DemoForEachCollection::x forEach {
        DemoString::x {
            greaterThan(1)
            lessThan(5)
        }
    }
}

nest example:

val validator = validator<DemoNested> {
    DemoNested::x nest {
        DemoString::x {
            greaterThan(1)
            lessThan(5)
        }
    }
}

nest delegate example:

val validator = validator<DemoNested> {
    DemoNested::x nest demoStringValidator
}

4. Combining validation results

YAVI supports a functional programming concept known as Applicative Functor. A sequence of validations are executed while accumulating the results (ConstraintViolation), even if some or all of these validations fail during the execution chain.

It is helpful when you want to combine validation results of multiple Value Objects to produce a new object. (Of course, it is also useful for any objects other than Value Objects.)

4.1. Validating with Applicative Functor

am.ik.yavi.fn.Validation<E, T> class is the implementation of Applicative Functor. E is the type of error and T is the type of target object.

It can be obtained by am.ik.yavi.core.ApplicativeValidator that can be converted from Validator by applicative() method.

Validator<User> validator = ValidatorBuilder.<User> of()
    .constraint(User::getName, "name", c -> c.notNull().lessThanOrEqual(20))
    // ...
    .build();

ApplicativeValidator<User> applicativeValidator = validator.applicative();

am.ik.yavi.core.Validated<T> is a shortcut of Validation<ConstraintViolation, T> which is specialized for Validator 's usage.

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

Validated<Email> emailValidated = emailValidator.applicative().validate(email);
Validated<PhoneNumber> phoneNumberValidated = phoneNumberValidator.applicative().validate(phoneNumber);

The validated target or constraint violations can be retrieved from the Validated instance as follows:

if (emailValidated.isValid()) {
    Email email = emailValidated.value(); // throws NoSuchElementException if it is invalid
} else {
    ConstraintViolations violations = emailValidated.errors(); // throws NoSuchElementException if it is valid
}
// or
Email email = emailValidated.orElseThrow(violations -> new ConstraintViolationsException(violations));

fold method is convenient if you want to create an instance of the same type regardless of the success or failure of the validation.

HttpStatus status = emailValidated.fold(violations -> HttpStatus.BAD_REQUEST, email -> HttpStatus.OK);

4.2. Combining Validation / Validated objects

Validation / Validated objects can be combined to produce a new object. In the bellow example, ContactInfo instance is created using Email and PhoneNumber after validating them.

Validated<ContactInfo> contactInfoValidated = emailValidated.combine(phoneNumberValidated)
    .apply((em, ph) -> new ContactInfo(em, ph));
// or
Validated<ContactInfo> contactInfoValidated = Validations.combine(emailValidated, phoneNumberValidated)
    .apply((em, ph) -> new ContactInfo(em, ph));

The important thing here is that even if the validation of Email or PhoneNumber fails, all validation results are accumulated without shortcuts in the middle.

For example, if you put a blank space in Email and a in PhoneNumber and try to create a ContactInfo, the validation will fail, and you will get the following three ContraintViolation s:

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

Validation for 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.

5.1. Defining and obtaining an Arguments Validator instance

Arguments Validator, as the name implies, is a Validator for arguments.

A normal Validator, like Bean Validation, validates if the values are valid for the object to which the values are set, while the Arguments Validator validates that the arguments set to the Object are valid.

Since an object is created before validation in a normal Validator, there is a possibility of creating an object in an incomplete state temporarily. For example, even if the constraint of notNull() is imposed on the Validator side, if the null check is implemented in the constructor, this null check will work before the validation by Validator is executed.

The Arguments Validator creates an object after validation, so you don’t have to worry about an incomplete object.

public class Person {
    public Person(String name, String email, Integer age) {
        // ...
    }

    public static Arguments3Validator<String, String, Integer, Person> validator = ArgumentsValidatorBuilder
        .of(Person::new)
        .builder(b -> b
            ._string(Arguments1::arg1, "name", c -> c.notBlank().lessThanOrEqual(100)) // Constrains onf the first argument of Person::new
            ._string(Arguments2::arg2, "email", c -> c.notBlank().lessThanOrEqual(100).email()) // Constrains onf the second argument of Person::new
            ._integer(Arguments3::arg3, "age", c -> c.greaterThanOrEqual(0).lessThan(200))) // Constrains onf the third argument of Person::new
        .build();
}

The Arguments Validator can be used as follows:

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

am.ik.yavi.arguments.Arguments1Validator to am.ik.yavi.arguments.Arguments16Validator are available.

Arguments Validator is useful, but it can be a bit verbose to define. Since it is Type-Safe, it is not possible to define something that does not fit the type, but it feels like a puzzle that fits the type. Building the Arguments Validator can be simplified by "Defining a Validator for a single value" and "Combining validators".

5.2. Validating method arguments

Arguments Validator can be used for validating method arguments as well.

// From https://beanvalidation.org/
public class UserService {
    public User createUser(/* @Email */ String email,
                         /* @NotNull */ String name) {
        // ...
    }
}

The arguments for UserService.createUser in the above example can be validated as follows:

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

UserService userService = new UserService();
Validated<User> userValidated = validator.validate(userService, "jdoe@example.com", "John Doe");
void cannot be used as return type while java.lang.Void is available.

5.3. Defining a Validator for a single value

If you just want to define Arguments1Validator for a single String or Integer etc, you can write:

StringValidator<String> nameValidator = StringValidatorBuilder
    .of("name", c -> c.notBlank().lessThanOrEqual(100))
    .build();  // -> extends Arguments1Validator<String, String>

StringValidator<String> emailValidator = StringValidatorBuilder
    .of("email", c -> c.notBlank().lessThanOrEqual(100).email())
    .build();  // -> extends Arguments1Validator<String, String>

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

Validated<String> nameValidated = nameValidator.validate("Jone Doe");
Validated<String> emailValidated = nameValidator.validate("jdoe@example.com");
Validated<Integer> ageValidated = nameValidator.validate(30);

You can convert it to a Value Object after validation by using the andThen method as follows:

StringValidator<Name> nameValidator = StringValidatorBuilder
    .of("name", c -> c.notBlank().lessThanOrEqual(100))
    .build()
    .andThen(name -> new Name(name)); // -> extends Arguments1Validator<String, Name>

StringValidator<Email> emailValidator = StringValidatorBuilder
    .of("email", c -> c.notBlank().lessThanOrEqual(100).email())
    .build()
    .andThen(email -> new Email(email)); // -> extends Arguments1Validator<String, Email>

IntegerValidator<Age> ageValidator = IntegerValidatorBuilder
    .of("age", c -> c.greaterThanOrEqual(0).lessThan(200))
    .build()
    .andThen(age -> new Age(age)); // -> extends Arguments1Validator<Integer, Age>

Validated<Name> nameValidated = nameValidator.validate("Jone Doe");
Validated<Email> emailValidated = nameValidator.validate("jdoe@example.com");
Validated<Age> ageValidated = nameValidator.validate(30);

If you want to create an Arguments1Validator for a List, you can "lift" a Validator for the element of the list as follows:

Arguments1Validator<Iterable<String>, List<Email>> emailsValidator = ArgumentsValidators.liftList(emailValidator);
Validated<List<Email>> emailsValidated = emailsValidator.validate(List.of("foo@example.com", "bar@example.com"));
// or
Validated<List<Email>> emailsValidated = emailValidator.liftList().validate(List.of("foo@example.com", "bar@example.com"));

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

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

6. Combining validators

ApplicativeValidator<T> and Arguments1Validator<S, T> we’ve seen so far extend ValueValidator<S, T>. (T is the type of the target object to be validated and S is the type of the source object to create the target object.)

ApplicativeValidator<T> is a ValueValidator<T, T>.

Multiple ValueValidator s can be combined to validate a larger object.

6.1. Splitting a validator for an object into small pieces

You can use split method to split a validator for the arguments of a constructor or a factory method for creating a large object into small validators and combine them.

Let’s take a look at an example.

Validator for constructor Person(String, String, Integer) is Arguments3Validator<String, String, Integer, Person>. This can be divided into two StringValidator<String> s and one IntegerValidator<Integer> and then combined.

It can be defined as bellow:

public class Person {
    public Person(String name, String email, Integer age) {
        // ...
    }
}

StringValidator<String> nameValidator = /* see examples above */;
StringValidator<String> emailValidator = /* see examples above */;
IntegerValidator<Integer> ageValidator = /* see examples above */;

Arguments3Validator<String, String, Integer, Person> personValidator = ArgumentsValidators
    .split(nameValidator, emailValidator, ageValidator)
    .apply(Person::new);
// or
Arguments3Validator<String, String, Integer, Person> personValidator = nameValidator
    .split(emailValidator)
    .split(ageValidator)
    .apply(Person::new);

The same goes for Validators for Value Objects.

public class Person {
    public Person(Name name, Email email, Age age) {
        // ...
    }
}

StringValidator<Name> nameValidator = /* see examples above */;
StringValidator<Email> emailValidator = /* see examples above */;
IntegerValidator<Age> ageValidator = /* see examples above */;

Arguments3Validator<String, String, Integer, Person> personValidator = ArgumentsValidators
    .split(nameValidator, emailValidator, ageValidator)
    .apply(Person::new);
// or
Arguments3Validator<String, String, Integer, Person> personValidator = nameValidator
    .split(emailValidator)
    .split(ageValidator)
    .apply(Person::new);

The created Validator can be used as follows:

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

Since the arguments of ArgumentsValidators#split are ValueValidator s rather than Arguments1Validator s, you can also combine ApplicativeValidator s as follows:

ApplicativeValidator<Email> emailValidator = /* see examples above */;
ApplicativeValidator<PhoneNumber> phoneNumberValidator = /* see examples above */;

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

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

6.2. Validating the source object before creating the target object

In a web application, you often get values from a Map, Form Object, or HttpServletRequest to create a Domain Object.

Arguments1Validator can validate the source object and then convert it to the target object using compose method.

For example, in the case where name, email and age are obtained from HttpServletRequest object as HTTP request parameters in Servlet, it can be defined as follows:

Argument1Validator<HttpServletRequest, Name> requestNameValidator = nameValidator
    .compose(req -> req.getParameter("name"));
Argument1Validator<HttpServletRequest, Email> requestEmailValidator = emailValidator
    .compose(req -> req.getParameter("email"));
Argument1Validator<HttpServletRequest, Age> requestAgeValidator = ageValidator
    .compose(req -> Integer.valueOf(req.getParameter("age")));

HttpServletRequest request = ...;
Validated<Name> nameValidated = requestNameValidator.validate(request);
Validated<Email> emailValidated = requestEmailValidator.validate(request);
Validated<Age> ageValidated = requestAgeValidator.validate(request);

You can combine these Validators with combine method as follows to create a Validator for Person object:

Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = ArgumentsValidators
    .combine(requestNameValidator, requestEmailValidator, requestAgeValidator)
    .apply(Person::new);
// or
Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = requestNameValidator
    .combine(requestEmailValidator)
    .combine(requestAgeValidator)
    .apply(Person::new);

HttpServletRequest request = ...;
Validated<Person> personValidated = requestPersonValidator.validate(request);

This Validator can also be converted from Arguments3Validator<String, String, Integer, Person> as follows:

Arguments3Validator<String, String, Integer, Person> personValidator = /* see examples above */;
Arguments1Validator<HttpServletRequest, Person> requestPersonValidator = personValidator
    .compose(req -> Arguments.of(req.getParameter("name"), req.getParameter("email"), Integer.valueOf(req.getParameter("age"))));

By combining Validators in this way, you can create various patterns of Validators. Creating a small Arguments Validator not only allows you to validate the values before creating the object, but also makes it more reusable.

7. Built-in Constraints

YAVI includes many commonly used built-in constraints for various types.

7.1. String

Apply the constraints on the String as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    .constraint(/* Function from TargetClass to String  */, /* Field Name */, /* Constraints */)
    .build();

To avoid ambiguous type inferences, you can use explicit _string instead of constraint as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    ._string(/* Function from TargetClass to String  */, /* Field Name */, /* Constraints */)
    .build();

If you want to apply constraints to the more generic CharSequence, use _charSequence method.

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    ._charSequence(/* Function from TargetClass to CharSequence  */, /* Field Name */, /* Constraints */)
    .build();

7.1.1. notNull()

Checks that the target value is not null

Validator<Email> validator = ValidatorBuilder.<Email> of()
    .constraint(Email::asString, "email", c -> c.notNull())
    .build();

ConstraintViolations violations = validator.validate(new Email("yavi@example.com")); // Valid
ConstraintViolations violations = validator.validate(new Email(null)); // Invalid ("email" must not be null)
Message Key Default Message Format Args

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 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. 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.12. email()

Checks if the target value is a valid email address

Validator<Email> validator = ValidatorBuilder.<Email> of()
    .constraint(Email::asString, "email", c -> c.email())
    .build();

ConstraintViolations violations = validator.validate(new Email("yavi@example.com")); // Valid
ConstraintViolations violations = validator.validate(new Email(null)); // Valid
ConstraintViolations violations = validator.validate(new Email("example.com")); // Invalid ("email" must not be a valid email address)
Message Key Default Message Format Args

charSequence.email

"{0}" must be a valid email address

  • {0} …​ field name

  • {1} …​ the given value

7.1.13. password(…​)

Check if the target value meets the specified password policy

Validator<Password> validator = ValidatorBuilder.<Password> of()
        .constraint(Password::value, "password", c -> c.password(policy -> policy
                .uppercase()
                .lowercase()
                // or .required(PasswordPolicy.UPPERCASE, PasswordPolicy.LOWERCASE)
                .optional(1, PasswordPolicy.NUMBERS, PasswordPolicy.SYMBOLS)
                .build()))
        .build();

ConstraintViolations violations = validator.validate(new Password("Yavi123")); // Valid
ConstraintViolations violations = validator.validate(new Password(null)); // Valid
ConstraintViolations violations = validator.validate(new Password("yavi123")); // Invalid ("password" must meet Uppercase policy)
ConstraintViolations violations = validator.validate(new Password("yavi")); // Invalid ("password" must meet Uppercase policy, "password" must meet at least 1 policies from [Numbers, Symbols])
ConstraintViolations violations = validator.validate(new Password("")); // Invalid ("password" must meet Uppercase policy, "password" must meet Lowercase policy, "password" must meet at least 1 policies from [Numbers, Symbols])
Message Key Default Message Format Args

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.14. ipv4()

Check if the target value is a valid IPv4 address

Validator<IpAddress> validator = ValidatorBuilder.<IpAddress> of()
    .constraint(IpAddress::asString, "ipAddress", c -> c.ipv4())
    .build();

ConstraintViolations violations = validator.validate(new IpAddress("192.0.2.1")); // Valid
ConstraintViolations violations = validator.validate(new IpAddress(null)); // Valid
ConstraintViolations violations = validator.validate(new IpAddress("example.com")); // Invalid ("ipAddress" must not be a valid IPv4)
Message Key Default Message Format Args

charSequence.ipv4

"{0}" must be a valid IPv4

  • {0} …​ field name

  • {1} …​ the given value

7.1.15. ipv6()

Check if the target value is a valid IPv6 address

Validator<IpAddress> validator = ValidatorBuilder.<IpAddress> of()
    .constraint(IpAddress::asString, "ipAddress", c -> c.ipv6())
    .build();

ConstraintViolations violations = validator.validate(new IpAddress("2001:0db8:bd05:01d2:288a:1fc0:0001:10ee")); // Valid
ConstraintViolations violations = validator.validate(new IpAddress(null)); // Valid
ConstraintViolations violations = validator.validate(new IpAddress("192.0.2.1")); // Invalid ("ipAddress" must not be a valid IPv6)
Message Key Default Message Format Args

charSequence.ipv6

"{0}" must be a valid IPv6

  • {0} …​ field name

  • {1} …​ the given value

7.1.16. url()

Check if the target value is a valid URL

Validator<Url> validator = ValidatorBuilder.<Url> of()
    .constraint(Url::asString, "url", c -> c.url())
    .build();

ConstraintViolations violations = validator.validate(new Url("https://yavi.ik.am")); // Valid
ConstraintViolations violations = validator.validate(new Url(null)); // Valid
ConstraintViolations violations = validator.validate(new Url("yavi.ik.am")); // Invalid ("url" must be a valid URL)
Message Key Default Message Format Args

charSequence.url

"{0}" must be a valid URL

  • {0} …​ field name

  • {1} …​ the given value

7.1.17. luhn()

Checks if the digits within the target value pass the Luhn checksum algorithm (see also Luhn algorithm).

Validator<CreditCard> validator = ValidatorBuilder.<CreditCard> of()
    .constraint(CreditCard::number, "creditCardNumber", c -> c.luhn())
    .build();

ConstraintViolations violations = validator.validate(new CreditCard("4111111111111111")); // Valid
ConstraintViolations violations = validator.validate(new CreditCard(null)); // Valid
ConstraintViolations violations = validator.validate(new CreditCard("4111111111111112")); // Invalid (the check digit for "creditCardNumber" is invalid, Luhn checksum failed)
Message Key Default Message Format Args

charSequence.luhn

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.18. isByte()

Check if the target value can be parsed as a Byte value

Validator<UserId> validator = ValidatorBuilder.<UserId> of()
    .constraint(UserId::asString, "userId", c -> c.isByte())
    .build();

ConstraintViolations violations = validator.validate(UserId.valueOf("127")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of a byte)
Message Key Default Message Format Args

charSequence.byte

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.19. isShort()

Check if the target value can be parsed as a Short value

Validator<UserId> validator = ValidatorBuilder.<UserId> of()
    .constraint(UserId::asString, "userId", c -> c.isShort())
    .build();

ConstraintViolations violations = validator.validate(UserId.valueOf("32767")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of a short)
Message Key Default Message Format Args

charSequence.short

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.20. isInteger()

Check if the target value can be parsed as an Integer value

Validator<UserId> validator = ValidatorBuilder.<UserId> of()
    .constraint(UserId::asString, "userId", c -> c.isInteger())
    .build();

ConstraintViolations violations = validator.validate(UserId.valueOf("2147483647")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of an integer)
Message Key Default Message Format Args

charSequence.integer

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.21. isLong()

Check if the target value can be parsed as a Long value

Validator<UserId> validator = ValidatorBuilder.<UserId> of()
    .constraint(UserId::asString, "userId", c -> c.isLong())
    .build();

ConstraintViolations violations = validator.validate(UserId.valueOf("9223372036854775807")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of a long)
Message Key Default Message Format Args

charSequence.long

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.22. isFloat()

Check if the target value can be parsed as a Float value

Validator<Money> validator = ValidatorBuilder.<Money> of()
    .constraint(Money::asString, "money", c -> c.isFloat())
    .build();

ConstraintViolations violations = validator.validate(Money.valueOf("0.1")); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf("a")); // Invalid ("money" must be a valid representation of a float)
Message Key Default Message Format Args

charSequence.float

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.23. isDouble()

Check if the target value can be parsed as a Double value

Validator<Money> validator = ValidatorBuilder.<Money> of()
    .constraint(Money::asString, "money", c -> c.isDouble())
    .build();

ConstraintViolations violations = validator.validate(Money.valueOf("0.1")); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf("a")); // Invalid ("money" must be a valid representation of a double)
Message Key Default Message Format Args

charSequence.double

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.24. isBigInteger()

Check if the target value can be parsed as a BigInteger value

Validator<UserId> validator = ValidatorBuilder.<UserId> of()
    .constraint(UserId::asString, "userId", c -> c.isBigInteger())
    .build();

ConstraintViolations violations = validator.validate(UserId.valueOf("127")); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(UserId.valueOf("a")); // Invalid ("userId" must be a valid representation of a big integer)
Message Key Default Message Format Args

charSequence.bigInteger

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.25. isBigDecimal()

Check if the target value can be parsed as a BigDecimal value

Validator<Money> validator = ValidatorBuilder.<Money> of()
    .constraint(Money::asString, "money", c -> c.isBigDecimal())
    .build();

ConstraintViolations violations = validator.validate(Money.valueOf("50.0")); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf(null)); // Valid
ConstraintViolations violations = validator.validate(Money.valueOf("a")); // Invalid ("money" must be a valid representation of a big decimal)
Message Key Default Message Format Args

charSequence.bigDecimal

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

  • {0} …​ field name

  • {1} …​ the given value

7.1.26. codePoints(…​)

Checks if the target value is in the specified set of code points. Code points can be specified as allowed characters (whitelist) or prohibited characters (blacklist).

A set of code points is represented by am.ik.yavi.constraint.charsequence.CodePoints interface, and there is am.ik.yavi.constraint.charsequence.CodePoints.CodePointsSet interface that represents the set using java.util.Set and am.ik.yavi.constraint.charsequence.CodePoints.CodePointsRanges interface that represents a list of code point ranges.

For example, a code point set consisting of "A, B, C, D, a, b, c, d" is expressed as follows:

CodePointsSet<String> codePoints = () -> Set.of(
        0x0041 /* A */, 0x0042 /* B */, 0x0043 /* C */, 0x0044 /* D */,
        0x0061 /* a */, 0x0062 /* b */, 0x0063 /* c */, 0x0064 /* d */);

Or:

CodePointsRanges<String> codePoints = () -> List.of(
        Range.of(0x0041/* A */, 0x0044 /* D */),
        Range.of(0x0061/* a */, 0x0064 /* d */));

For consecutive code points, the latter is overwhelmingly more memory efficient.

If you want to regard the code point set as a white list (allowed characters), specify as follows:

Validator<Message> validator = ValidatorBuilder.<Message> of()
    .constraint(Message::getText, "text", c -> c.codePoints(codePoints).asWhiteList())
    .build();

ConstraintViolations violations = validator.validate(new Message("aBCd")); // Valid
ConstraintViolations violations = validator.validate(new Message(null)); // Valid
ConstraintViolations violations = validator.validate(new Message("aBCe")); // Invalid ("[e]" is/are not allowed for "text")

If you want to regard the code point set as a blacklist (prohibited characters), specify as follows:

Validator<Message> validator = ValidatorBuilder.<Message> of()
    .constraint(Message::getText, "text", c -> c.codePoints(codePoints).asBlackList())
    .build();

ConstraintViolations violations = validator.validate(new Message("hello")); // Valid
ConstraintViolations violations = validator.validate(new Message(null)); // Valid
ConstraintViolations violations = validator.validate(new Message("hallo")); // Invalid ("[a]" is/are not allowed for "text")
Message Key Default Message Format Args

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.27. Advanced character length check

YAVI counts the input characters as the number of characters as it looks in the constraints on the number of characters. Recently, as a result of defining various characters on Unicode, the visual size and the return value of String#length method are quite different.

YAVI supports the following character types:

  • Surrogate pair

  • Combining character

  • Variation Selector

    • IVS (Ideographic Variation Sequence)

    • SVS (Standardized Variation Sequence)

    • FVS (Mongolian Free Variation Selector)

  • Emoji

YAVI will perform a constraint check on surrogate pairs and combining characters with the number of characters as they look. (For Emoji, IVS, SVS and FVS, the size is not checked as it looks by default due to the performance.)

Let’s look at a typical example.

Surrogate pair

𠮷野屋 is an example of a surrogate pair. It looks like 3 characters, but the result of length method is 4 (\uD842\uDFB7野屋).

System.out.println("𠮷野屋".length()); // 4 (\uD842\uDFB7野屋)

Since YAVI treats the character length as the code point length, this character string is regarded as 3 characters.

Validator<Message> validator = ValidatorBuilder.<Message> of()
    .constraint(Message::getText, "text", c -> c.lessThanOrEqual(3))
    .build();

ConstraintViolations = validator.validate(new Message("𠮷野屋")); // Valid
Combining character

モジ is an example of a combining character. Although it looks like 2 characters, and dakuten() are combined, and the result of length() is 3 (モシ\u3099).

System.out.println("モジ".length()); // 3 (モシ\u3099)

YAVI considers this string to be 2 characters by default.

Validator<Message> validator = ValidatorBuilder.<Message> of()
    .constraint(Message::getText, "text", c -> c.lessThanOrEqual(2))
    .build();

ConstraintViolations = validator.validate(new Message("モジ")); // Valid

YAVI uses java.text.Normalizer and normalizes with java.text.Normalizer.Form#NFC by default. This behavior can be changed as follows: (If null is set, it will not be normalized)

Validator<Message> validator = ValidatorBuilder.<Message> of()
    .constraint(Message::getText, "text", c -> c.normalizer(normalizerForm)
                .lessThanOrEqual(2))
    .build();
Variation Selector

𠮟󠄀 is an example of an Ideographic Variation Sequence. Variant selectors cannot be normalized with Normalizer. It looks like 1 character, but when expressed in UTF-16, it is "D842 DF9F DB40 DD00", so the result of length() is 4.

System.out.println("𠮟󠄀".length()); // 4 (\uD842\uDF9F\uDB40\uDD00󠄀)

YAVI does not consider this character as a single character by default to prevent performance degradation.

Validator<Message> validator = ValidatorBuilder.<Message> of()
        .constraint(Message::getText, "text", c -> c.lessThanOrEqual(1))
        .build();
ConstraintViolations = validator.validate(new Message("𠮟󠄀")); // Invalid (The size of "text" must be less than or equal to 1. The given size is 2)

You can ignore (delete) the code point of 0xE0100-0xE01EF, which is the range of IVS from the target string, as follows. This way you can regard this string as just a surrogate pair.

Validator<Message> validator = ValidatorBuilder.<Message> of()
        .constraint(Message::getText, "text", c -> c.variant(opts -> opts.ivs(IdeographicVariationSequence.IGNORE))
                .lessThanOrEqual(1))
        .build();
ConstraintViolations = validator.validate(new Message("𠮟󠄀")); // Valid

SVS and FVS can be handled in the same way.

Emoji

Emoji is crazy. The apparent number of characters and the number of code points are far away.

Let me give you examples.

System.out.println("❤️".length()); // 2
System.out.println("🤴🏻".length()); // 4
System.out.println("👨‍👦".length()); // 5
System.out.println("️👨‍👨‍👧‍👦".length()); // 12
System.out.println("🏴󠁧󠁢󠁥󠁮󠁧󠁿".length()); // 14 (WTH!)

YAVI can try to count these Emojis as one character as much as possible. There is no guarantee, but all emojis defined in Emoji 12.0 have been tested.

This process is expensive and is not enabled by default. To enable this feature, specify the emoji() method as follows:

Validator<Message> validator = ValidatorBuilder.<Message> of()
    .constraint(Message::getText, "text", c -> c.emoji().lessThanOrEqual(1))
    .build();
ConstraintViolations = validator.validate(new Message("❤️")); // Valid
ConstraintViolations = validator.validate(new Message("🤴🏻")); // Valid
ConstraintViolations = validator.validate(new Message("👨‍👦")); // Valid
ConstraintViolations = validator.validate(new Message("️👨‍👨‍👧‍👦")); // Valid
ConstraintViolations = validator.validate(new Message("🏴󠁧󠁢󠁥󠁮󠁧󠁿")); // Valid

7.1.28. asByteArray()

If there is a discrepancy between the apparent character length and the actual code point length, the appearance restrictions are OK, but the size stored in the database may be exceeded. In YAVI, you can check the byte length in addition to the visual size.

Validator<Message> validator = ValidatorBuilder.<Message> of()
    .constraint(Message::getText, "text", c -> c.emoji().lessThanOrEqual(3)
            .asByteArray().lessThanOrEqual(16))
    .build();

ConstraintViolations = validator.validate(new Message("I❤️☕️️")); // Valid
ConstraintViolations = validator.validate(new Message("❤️❤️❤️️️")); // Invalid (The byte size of "text" must be less than or equal to 16. The given size is 24)
Message Key Default Message Format Args

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

numeric.greaterThan

"{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 10)
Message Key Default Message Format Args

numeric.lessThan

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

  • {0} …​ field name

  • {1} …​ the specified size

  • {2} …​ the given value

7.3. Boolean

Apply the constraints on the Boolean as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    .constraint(/* Function from TargetClass to Boolean  */, /* Field Name */, /* Constraints */)
    .build();

To avoid ambiguous type inferences, you can use explicit _boolean instead of constraint as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    ._boolean(/* Function from TargetClass to Boolean  */, /* Field Name */, /* Constraints */)
    .build();

7.3.1. notNull()

Checks that the target value is not null

Validator<Confirmation> validator = ValidatorBuilder.<Confirmation> of()
    .constraint(Confirmation::isConfirmed, "confirmed", c -> c.notNull())
    .build();

ConstraintViolations violations = validator.validate(new Confirmation(true)); // Valid
ConstraintViolations violations = validator.validate(new Confirmation(null)); // Invalid ("confirmed" must not be null)
Message Key Default Message Format Args

object.notNull

"{0}" must not be null

  • {0} …​ field name

  • {1} …​ the given value

7.3.2. isNull()

Checks that the target value is null

Validator<Confirmation> validator = ValidatorBuilder.<Confirmation> of()
    .constraint(Confirmation::isConfirmed, "confirmed", c -> c.isNull())
    .build();

ConstraintViolations violations = validator.validate(new Confirmation(null)); // Valid
ConstraintViolations violations = validator.validate(new Confirmation(true)); // Invalid ("confirmed" must be null)
Message Key Default Message Format Args

object.isNull

"{0}" must be null

  • {0} …​ field name

  • {1} …​ the given value

7.3.3. isTrue()

Checks that the target value is true

Validator<Confirmation> validator = ValidatorBuilder.<Confirmation> of()
    .constraint(Confirmation::isConfirmed, "confirmed", c -> c.isTrue())
    .build();

ConstraintViolations violations = validator.validate(new Confirmation(true)); // Valid
ConstraintViolations violations = validator.validate(new Confirmation(null)); // Valid
ConstraintViolations violations = validator.validate(new Confirmation(false)); // Invalid ("confirmed" must be true)
Message Key Default Message Format Args

boolean.isTrue

"{0}" must be true

  • {0} …​ field name

  • {1} …​ the given value

7.3.4. isFalse()

Checks that the target value is false

Validator<Rented> validator = ValidatorBuilder.<Rented> of()
    .constraint(Rented::isRented, "rented", c -> c.isFalse())
    .build();

ConstraintViolations violations = validator.validate(new Rented(false)); // Valid
ConstraintViolations violations = validator.validate(new Rented(null)); // Valid
ConstraintViolations violations = validator.validate(new Rented(true)); // Invalid ("rented" must be false)
Message Key Default Message Format Args

boolean.isFalse

"{0}" must be false

  • {0} …​ field name

  • {1} …​ the given value

7.4. Collection

Apply the constraints on the Collection as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    .constraint(/* Function from TargetClass to Collection  */, /* Field Name */, /* Constraints */)
    .build();

To avoid ambiguous type inferences, you can use explicit _collection instead of constraint as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    ._collection(/* Function from TargetClass to Collection  */, /* Field Name */, /* Constraints */)
    .build();

7.4.1. notNull()

Checks that the target value is not null

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

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Invalid ("histories" must not be null)
Message Key Default Message Format Args

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<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.4.3. notEmpty()

Checks that the target value is not empty

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
        .constraint(Histories::asList, "histories", c -> c.notEmpty())
        .build();

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Invalid ("histories" must not be empty)
ConstraintViolations violations = validator.validate(new Histories(List.of())); // Invalid ("histories" must not be empty)
Message Key Default Message Format Args

container.notEmpty

"{0}" must not be empty

  • {0} …​ field name

  • {1} …​ the given value

7.4.4. fixedSize(int)

Checks if the target value’s size is the specified size

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
        .constraint(Histories::asList, "histories", c -> c.fixedSize(2))
        .build();

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History()))); // Invalid (The size of "histories" must be 2. The given size is 1)
Message Key Default Message Format Args

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.4.5. greaterThan(int)

Checks if the target value’s size is greater than the specified size

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
        .constraint(Histories::asList, "histories", c -> c.greaterThan(1))
        .build();

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History()))); // Invalid (The size of "histories" must be greater than 1. The given size is 1)
Message Key Default Message Format Args

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.4.6. greaterThanOrEqual(int)

Checks if the target value’s size is greater than or equals to the specified size

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
        .constraint(Histories::asList, "histories", c -> c.greaterThanOrEqual(2))
        .build();

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History()))); // Invalid (The size of "histories" must be greater than or equal to 2. The given size is 1)
Message Key Default Message Format Args

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.4.7. lessThan(int)

Checks if the target value’s size is less than the specified size

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
        .constraint(Histories::asList, "histories", c -> c.lessThan(3))
        .build();

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History(), new History()))); // Invalid (The size of "histories" must be less than 3. The given size is 3)
Message Key Default Message Format Args

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.4.8. lessThanOrEqual(int)

Checks if the target value’s size is less than or equals to the specified size

Validator<Histories> validator = ValidatorBuilder.<Histories> of()
        .constraint(Histories::asList, "histories", c -> c.lessThanOrEqual(2))
        .build();

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History()))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(), new History(), new History()))); // Invalid (The size of "histories" must be less than or equal to 2. The given size is 3)
Message Key Default Message Format Args

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.4.9. contains(…​)

Checks if the target value contains the specified value

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

ConstraintViolations violations = validator.validate(new Histories(List.of(new History(1), new History(2)))); // Valid
ConstraintViolations violations = validator.validate(new Histories(null)); // Valid
ConstraintViolations violations = validator.validate(new Histories(List.of(new History(3), new History(4), new History(5)))); // Invalid ("histories" must contain History{revision=2})
Message Key Default Message Format Args

collection.contains

"{0}" must contain {1}

  • {0} …​ field name

  • {1} …​ the specified value

  • {2} …​ the given value

7.5. Map

Apply the constraints on the Map as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    .constraint(/* Function from TargetClass to Map  */, /* Field Name */, /* Constraints */)
    .build();

To avoid ambiguous type inferences, you can use explicit _map instead of constraint as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    ._map(/* Function from TargetClass to Map  */, /* Field Name */, /* Constraints */)
    .build();

7.5.1. notNull()

Checks that the target value is not null

Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
        .constraint(CodeMap::asMap, "codeMap", c -> c.notNull())
        .build();

ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Invalid ("codeMap" must not be null)
Message Key Default Message Format Args

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<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.5.3. notEmpty()

Checks that the target value is not empty

Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
        .constraint(CodeMap::asMap, "codeMap", c -> c.notEmpty())
        .build();

ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Invalid ("codeMap" must not be empty)
ConstraintViolations violations = validator.validate(new CodeMap(Map.of())); // Invalid ("codeMap" must not be empty)
Message Key Default Message Format Args

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<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.5.5. greaterThan(int)

Checks if the target value’s size is greater than the specified size

Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
        .constraint(CodeMap::asMap, "codeMap", c -> c.greaterThan(1))
        .build();

ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A"))); // Invalid (The size of "codeMap" must be greater than 1. The given size is 1)
Message Key Default Message Format Args

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<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.5.7. lessThan(int)

Checks if the target value’s size is less than the specified size

Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
        .constraint(CodeMap::asMap, "codeMap", c -> c.lessThan(3))
        .build();

ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B", "c", "C"))); // Invalid (The size of "histories" must be less than 3. The given size is 3)
Message Key Default Message Format Args

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<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.5.9. containsKey(…​)

Checks if the target value contains the specified key

Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
        .constraint(CodeMap::asMap, "codeMap", c -> c.containsKey("b"))
        .build();

ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("c", "C"))); // Invalid ("codeMap" must contain key b)
Message Key Default Message Format Args

map.containsKey

"{0}" must contain key {1}

  • {0} …​ field name

  • {1} …​ the specified key

  • {2} …​ the given value

7.5.10. containsValue(…​)

Checks if the target value contains the specified value

Validator<CodeMap> validator = ValidatorBuilder.<CodeMap> of()
        .constraint(CodeMap::asMap, "codeMap", c -> c.containsValue("B"))
        .build();

ConstraintViolations violations = validator.validate(new CodeMap(Map.of("a", "A", "b", "B"))); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(null)); // Valid
ConstraintViolations violations = validator.validate(new CodeMap(Map.of("c", "C"))); // Invalid ("codeMap" must contain value B)
Message Key Default Message Format Args

map.containsValue

"{0}" must contain value {1}

  • {0} …​ field name

  • {1} …​ the specified value

  • {2} …​ the given value

7.6. Object

Apply the constraints on any Object as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    .constraint(/* Function from TargetClass to Object  */, /* Field Name */, /* Constraints */)
    .build();

To avoid ambiguous type inferences, you can use explicit _object instead of constraint as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    ._object(/* Function from TargetClass to Object  */, /* Field Name */, /* Constraints */)
    .build();

If you want to apply constraints on target class itself (e.g. [cross-filed-validation]), you can use constraintOnTarget as follows:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    .constraintOnTarget(/* Field Name */, /* Constraints */)
    .build();

It is equivalent to bellow:

Validator<TargetClass> validator = ValidatorBuilder.<TargetClass> of()
    ._object(Function.identity(), /* Field Name */, /* Constraints */)
    .build();

7.6.1. notNull()

Checks that the target value is not null

Validator<CreatedAt> validator = ValidatorBuilder.<CreatedAt> of()
    .constraint(CreatedAt::asInstant, "createdAt", c -> c.notNull())
    .build();

ConstraintViolations violations = validator.validate(new CreatedAt(Instant.now())); // Valid
ConstraintViolations violations = validator.validate(new CreatedAt(null)); // Invalid ("createdAt" must not be null)
Message Key Default Message Format Args

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<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.6.3. password()

Check if the target value meets the specified custom password policy (e.g. Cross-field check)

PasswordPolicy<Account> passwordPolicy = PasswordPolicy.of("DoNotIncludeUsername",
        account -> {
            String username = account.getUsername();
            String password = account.getPassword();
            if (username == null || password == null) {
                return true;
            }
            return !password.toUpperCase().contains(username.toUpperCase());
        });
Validator<Account> validator = ValidatorBuilder.<Account> of()
        .constraintOnTarget("password", c -> c.password(policy -> policy
                .required(passwordPolicy)
                .build()))
        .build();

ConstraintViolations violations = validator.validate(new Account("foo", "Bar1234")); // Valid
ConstraintViolations violations = validator.validate(new Account(null, null)); // Valid
ConstraintViolations violations = validator.validate(new Account("foo", "Foo1234")); // Invalid ("password" must meet DoNotIncludeUsername policy)
Message Key Default Message Format Args

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

8. Annotation Processor

YAVI provides Annotation Processor to generates classes which defines meta classes that implement am.ik.yavi.meta.ConstraintMeta for each target which is annotated with am.ik.yavi.meta.ConstraintTarget or am.ik.yavi.meta.ConstraintArguments.

When you have a class as following, run mvn clean compile:

package com.example;

import am.ik.yavi.meta.ConstraintTarget;

public class UserForm {

    private String name;

    private String email;

    private Integer age;

    @ConstraintTarget
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ConstraintTarget
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @ConstraintTarget
    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

You’ll see target/generated-sources/annotations/com/example/_UserFormMeta.java is automatically generated.

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

8.1. Annotating @ConstraintTarget

@ConstraintTarget is used to generate meta classes that can be used with ValidatorBuilder#constraint. + It can be annotated on methods or constructor arguments.

Xyz class that is annotated with @ConstraintTarget will generate _XyzMeta.

Validator can be built as follows:

Validator<UserForm> validator = ValidatorBuilder.<UserForm> of()
    .constraint(_UserFormMeta.NAME, c -> c.notEmpty())
    .constraint(_UserFormMeta.EMAIL, c -> c.notEmpty().email())
    .constraint(_UserFormMeta.AGE, c -> c.greaterThan(0))
    .build();

No more string literal is required and it’s really type-safe.

The example above is available for the following 4 ways of generation.

8.1.1. @ConstraintTarget on getters

If you like "Java Beans" style, this way would be straightforward.

package com.example;

import am.ik.yavi.meta.ConstraintTarget;

public class UserForm {

    private String name;

    private String email;

    private Integer age;

    @ConstraintTarget
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ConstraintTarget
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @ConstraintTarget
    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

will generate

package com.example;

// Generated at 2020-02-11T17:37:47.866300+09:00
public class _UserFormMeta {

    public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> NAME = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "name";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
            return com.example.UserForm::getName;
        }
    };

    public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "email";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
            return com.example.UserForm::getEmail;
        }
    };

    public static final am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "age";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.Integer> toValue() {
            return com.example.UserForm::getAge;
        }
    };
}

8.1.2. on constructor arguments + getters

If you prefer an immutable class like Value Object, annotate @ConstraintTarget on constructor arguments.

package com.example;

import am.ik.yavi.meta.ConstraintTarget;

public class UserForm {

    private final String name;

    private final String email;

    private final Integer age;

    public UserForm(@ConstraintTarget String name,
                    @ConstraintTarget String email,
                    @ConstraintTarget Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }


    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public Integer getAge() {
        return age;
    }
}

will generate

package com.example;

// Generated at 2020-02-11T17:59:03.892823+09:00
public class _UserFormMeta {

    public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> NAME = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "name";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
            return com.example.UserForm::getName;
        }
    };

    public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "email";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
            return com.example.UserForm::getEmail;
        }
    };

    public static final am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "age";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.Integer> toValue() {
            return com.example.UserForm::getAge;
        }
    };
}

This is as same as the first example.

8.1.3. on constructor arguments + non-getters

You may not like "getter" style for immutable objects. You can use filed name as method name using getter = false. This will be a great fit for Records.

package com.example;

import am.ik.yavi.meta.ConstraintTarget;

public class UserForm {

    private final String name;

    private final String email;

    private final Integer age;

    public UserForm(@ConstraintTarget(getter = false) String name,
                    @ConstraintTarget(getter = false) String email,
                    @ConstraintTarget(getter = false) Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }


    public String name() {
        return name;
    }

    public String email() {
        return email;
    }

    public Integer age() {
        return age;
    }
}

will generate

package com.example;

// Generated at 2020-02-11T18:00:37.868495+09:00
public class _UserFormMeta {

    public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> NAME = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "name";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
            return com.example.UserForm::name;
        }
    };

    public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "email";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
            return com.example.UserForm::email;
        }
    };

    public static final am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "age";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.Integer> toValue() {
            return com.example.UserForm::age;
        }
    };
}

8.1.4. on constructor arguments + field access

You may prefer accessing fields directly rather than accessors, then use field = true.

package com.example;

import am.ik.yavi.meta.ConstraintTarget;

public class UserForm {

    final String name;

    final String email;

    final Integer age;

    public UserForm(@ConstraintTarget(field = true) String name,
                    @ConstraintTarget(field = true) String email,
                    @ConstraintTarget(field = true) Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
}

will generate

package com.example;

// Generated at 2020-02-11T18:02:47.124191+09:00
public class _UserFormMeta {

    public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> NAME = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "name";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
            return x  -> x.name;
        }
    };

    public static final am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "email";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.String> toValue() {
            return x  -> x.email;
        }
    };

    public static final am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<com.example.UserForm>() {

        @Override
        public String name() {
            return "age";
        }

        @Override
        public java.util.function.Function<com.example.UserForm, java.lang.Integer> toValue() {
            return x  -> x.age;
        }
    };
}

8.2. Annotating @ConstraintArguments

@ConstraintArguments is used to generate meta classes that can be used with ArgumentsValidatorBuilder. + It can be annotated on constructors or methods.

The constructor of Xyz class that is annotated with @ConstraintTarget will generate _XyzArgumentsMeta.

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

8.2.1. Validating Constructor Arguments

package com.example;

import am.ik.yavi.meta.ConstraintArguments;

public class User {

    private final String name;

    private final String email;

    private final int age;

    @ConstraintArguments
    public User(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
}

will generate

package com.example;

// Generated at 2020-02-11T18:22:15.164882+09:00
public class _UserArgumentsMeta {

    public static final am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>> NAME = new am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>>() {

        @Override
        public String name() {
            return "name";
        }

        @Override
        public java.util.function.Function<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>, java.lang.String> toValue() {
            return am.ik.yavi.arguments.Arguments1::arg1;
        }
    };

    public static final am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>>() {

        @Override
        public String name() {
            return "email";
        }

        @Override
        public java.util.function.Function<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>, java.lang.String> toValue() {
            return am.ik.yavi.arguments.Arguments2::arg2;
        }
    };

    public static final am.ik.yavi.meta.IntegerConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>>() {

        @Override
        public String name() {
            return "age";
        }

        @Override
        public java.util.function.Function<am.ik.yavi.arguments.Arguments3<java.lang.String, java.lang.String, java.lang.Integer>, java.lang.Integer> toValue() {
            return am.ik.yavi.arguments.Arguments3::arg3;
        }
    };
}

ArgumentsNValidator can be built as follows:

Arguments3Validator<String, String, Integer, User> validator = ArgumentsValidatorBuilder
    .of(User::new)
    .builder(b -> b
        .constraint(_UserArgumentsMeta.NAME, c -> c.notEmpty())
        .constraint(_UserArgumentsMeta.EMAIL, c -> c.notEmpty().email())
        .constraint(_UserArgumentsMeta.AGE, c -> c.greaterThan(0))
    )
    .build();

8.2.2. Validating Method Arguments

package com.example;

import am.ik.yavi.meta.ConstraintArguments;

public class UserService {

    @ConstraintArguments
    public User createUser(String name, String email, int age) {
        return new User(name, email, age);
    }
}

will generate

package com.example;

// Generated at 2020-02-11T18:40:53.554587+09:00
public class _UserServiceCreateUserArgumentsMeta {

    public static final am.ik.yavi.meta.ObjectConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, com.example.UserService> USERSERVICE = new am.ik.yavi.meta.ObjectConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, com.example.UserService>() {

        @Override
        public String name() {
            return "userService";
        }

        @Override
        public java.util.function.Function<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, com.example.UserService> toValue() {
            return am.ik.yavi.arguments.Arguments1::arg1;
        }
    };

    public static final am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>> NAME = new am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>>() {

        @Override
        public String name() {
            return "name";
        }

        @Override
        public java.util.function.Function<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, java.lang.String> toValue() {
            return am.ik.yavi.arguments.Arguments2::arg2;
        }
    };

    public static final am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>> EMAIL = new am.ik.yavi.meta.StringConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>>() {

        @Override
        public String name() {
            return "email";
        }

        @Override
        public java.util.function.Function<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, java.lang.String> toValue() {
            return am.ik.yavi.arguments.Arguments3::arg3;
        }
    };

    public static final am.ik.yavi.meta.IntegerConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>> AGE = new am.ik.yavi.meta.IntegerConstraintMeta<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>>() {

        @Override
        public String name() {
            return "age";
        }

        @Override
        public java.util.function.Function<am.ik.yavi.arguments.Arguments4<com.example.UserService, java.lang.String, java.lang.String, java.lang.Integer>, java.lang.Integer> toValue() {
            return am.ik.yavi.arguments.Arguments4::arg4;
        }
    };
}

ArgumentsNValidator can be built as follows:

Arguments4Validator<UserService, String, String, Integer, User> validator = ArgumentsValidatorBuilder
    .of(UserService::createUser)
    .builder(b -> b
        .constraint(_UserServiceCreateUserArgumentsMeta.USERSERVICE, c -> c.notNull())
        .constraint(_UserServiceCreateUserArgumentsMeta.NAME, c -> c.notEmpty())
        .constraint(_UserServiceCreateUserArgumentsMeta.EMAIL, c -> c.notEmpty().email())
        .constraint(_UserServiceCreateUserArgumentsMeta.AGE, c -> c.greaterThan(0))
    )
    .build();

9. Integrations

Although YAVI doesn’t depend on anything, it has interfaces that magically fit into Framework.

9.1. Integration with BindingResult in Spring MVC

ConstraintViolations#apply accepts the Function Interface with the same arguments as Spring Framework’s BindingResult#rejectValue method and passes the violation messages.

To reflect validation results by YAVI in a Controller of Spring MVC, you can write the following code.

final Validator<UserForm> validator = ...;

@PostMapping(path = "users")
public String createUser(Model model, UserForm userForm, BindingResult bindingResult) {
    ConstraintViolations violations = validator.validate(userForm);
    if (!violations.isValid()) {
        violations.apply(bindingResult::rejectValue);
        return "userForm";
    }
    // ...
    return "redirect:/";
}

If you like a functional way, you can also write:

final Validator<UserForm> validator = ...;

@PostMapping(path = "users")
public String createUser(Model model, UserForm userForm, BindingResult bindingResult) {
    return validator.applicative()
        .validate(userForm)
        .fold(violations -> {
            ConstraintViolations.of(violations).apply(bindingResult::rejectValue);
            return "userForm";
        }, form -> {
            // ...
            return "redirect:/";
        });
}
List<ConstraintViolation> is passed to the function of the first argument of Validated#fold instead of ConstraintViolations. You can convert the List<ContraintViolation> to ConstraintViolations by using the ConstraintViolations#of method.

9.2. Converting ConstraintViolations to the format to serialize as a response body

ConstraintViolation is not suitable for serializing with a serializer like Jackson. Instead, you can use ConstraintViolation#detail method to convert it to a ViolationDetail object that is easy to serialize.

The ConstraintViolations#details method translates all ConstraintViolation s and returns List<ViolationDetail>.

final Validator<UserCreateRequest> validator = ...;

@PostMapping(path = "users")
public ResponseEntity<?> createUser(@RequestBody UserCreateRequest request) {
    ConstraintViolations violations = validator.validate(request);
    if (violations.isValid()) {
        User created = userService.create(request.toUser());
        return ResponseEntity.ok(created);
    } else {
        return ResponseEntity.badRequest().body(violations.details());
    }
}

If you like a functional way, you can also write:

final Validator<UserCreateRequest> validator = ...;

@PostMapping(path = "users")
public ResponseEntity<?> createUser(@RequestBody UserCreateRequest request) {
    return validator.applicative()
        .validate(request)
        .map(req -> userService.create(req.toUser()))
        .mapErrors(violations -> ConstraintViolations.of(violations).details())
        // or .mapError(ConstraintViolation::detail)
        .fold(details -> ResponseEntity.badRequest().body(details),
            created -> ResponseEntity.ok(created));
}
ViolationDetail works with GraalVM native image out of the box.

9.3. Integration with Spring WebFlux.fn

YAVI will be a great fit for Spring WebFlux.fn.

final Validator<UserCreateRequest> validator = ...;

public RouterFunction<ServerResponse> routes() {
    return RouterFunctions.route()
        .POST("/users", request -> request.bodyToMono(UserCreateRequest.class)
            .flatMap(body -> validator.applicative()
                .validate(body)
                .map(req -> userService.create(req.toUser()))
                .mapErrors(violations -> ConstraintViolations.of(violations).details())
                // or .mapError(ConstraintViolation::detail)
                .fold(details -> ServerResponse.badRequest().bodyValue(details),
                    created -> ServerResponse.ok().bodyValue(created))))
        .build();
}
YAVI was originally developed as a validator naturally fit with Spring WebFlux.fn.

9.4. Integration with MessageSource in Spring Framework

am.ik.yavi.message.MessageSourceMessageFormatter accepts the Functional Interface with the same arguments as Spring Framework’s MessageSource#getMessage. This allows you to delegate control of the message format of violation messages to Spring Framework.

MessageSourceMessageFormatter can be used as follows:

@RestController
public class OrderController {
    private final Validator<CartItem> validator;

    public OrderController(MessageSource messageSource) {
        MessageFormatter messageFormatter = new MessageSourceMessageFormatter(messageSource::getMessage);
        this.validator = ValidatorBuilder.<CartItem> of()
            .constraints(...)
            .messageFormatter(messageFormatter)
            .build();
    }
}

9.5. Managing ValidatorBuilder in an IoC Container

If you want to customize ValidatorBuilder and manage it with an IoC Container like Spring Framework, you can use am.ik.yavi.factory.ValidatorFactory.

The following is an example of defining a ValidatorFactory in Spring Framework:

@Bean
public ValidatorFactory yaviValidatorFactory(MessageSource messageSource) {
    MessageFormatter messageFormatter = new MessageSourceMessageFormatter(messageSource::getMessage);
    return new ValidatorFactory("." /* Message Key Separator */, messageFormatter);
}

The usage of a Validator would look like following:

@RestController
public class OrderController {
    private final Validator<CartItem> validator;

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

9.6. Obtaining a BiValidator

am.ik.yavi.core.BiValidator<T, E> is a BiConsumer<T, E>. T is the type of target object as usual and E is the type of errors object.

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

BiValidator can be obtained as below:

BiValidator<CartItem, Errors> validator = ValidatorBuilder.<CartItem> of()
    .constraint(...)
    .build(Errors::rejectValue);

There is a factory for BiValidator as well

@Bean
public BiValidatorFactory<Errors> biValidatorFactory() {
    return new BiValidatorFactory<>(Errors::rejectValues);
}

or, if you want to customize the builder

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

The usage of a BiValidator would look like following:

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

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