YAVI (pronounced jɑ-vάɪ) is a lambda based type safe validation for Java.
YAVI sounds as same as a Japanese slang "YABAI" that means awesome or awful depending on the context.
The concepts are
- No more reflection!
- No more annotation!
- No more Java Beans!
- Zero dependency!
If you are not a fan of Bean Validation, YAVI will be an awesome alternative.
For the migration from Bean Validation, refer the doc.
Add the following dependency in your pom.xml
<dependency>
<groupId>am.ik.yavi</groupId>
<artifactId>yavi</artifactId>
<version>0.2.5</version>
</dependency>
If you want to try a snapshot version, add the following repository:
<repository>
<id>sonatype-snapshots</id>
<name>Sonatype Snapshots</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
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);
violations.isValid(); // true or false
violations.forEach(x -> System.out.println(x.message()));
If you are using Kotlin, you can write a bit shorter using konstraint
method instead of constraint
val validator: Validator<User> = ValidatorBuilder.of<User>()
.konstraint(User::name) {
notNull() //
.lessThanOrEqual(20)
} //
.konstraint(User::email) {
notNull() //
.greaterThanOrEqual(5) //
.lessThanOrEqual(50) //
.email()
} //
.konstraint(User::age) {
notNull() //
.greaterThanOrEqual(0) //
.lessThanOrEqual(200)
}
.build()
Validator<Country> countryValidator = ValidatorBuilder.<Country> of() //
.nest(Country::getName, "name", c -> c.notBlank() //
.lessThanOrEqual(20))
.build();
Validator<City> cityValidator = ValidatorBuilder.<City> of() //
.nest(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();
Validator<User> validator = ValidatorBuilder.<User> of() //
.constraint(User::getName, "name", c -> c.notNull().message("name is required!") //
.greaterThanOrEqual(1).message("name is too small!") //
.lessThanOrEqual(20).message("name is too large!")) //
.build()
public enum IsbnConstraint implements CustomConstraint<String> {
SINGLETON;
@Override
public boolean test(String s) {
return ISBNValidator.isISBN13(s);
}
@Override
public String messageKey() {
return "custom.isbn13";
}
@Override
public String defaultMessageFormat() {
return "\"{0}\" must be ISBN13 format";
}
}
Validator<Book> book = ValidatorBuilder.<Book> of() //
.constraint(Book::getTitle, "title", c -> c.notBlank() //
.lessThanOrEqual(64)) //
.constraint(Book::getIsbn, "isbn", c -> c.notBlank()//
.predicate(IsbnConstraint.SINGLETON))
.build(); //
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.to > range.from, "to", "to.isGreaterThanFrom", "\"to\" must be greater than \"from\".") //
.build();
Either<ConstraintViolations, User> either = validator.validateToEither(user);
Optional<ConstraintViolations> violations = either.left();
Optional<User> user = either.right();
HttpStatus status = either.fold(v -> HttpStatus.BAD_REQUEST, u -> HttpStatus.OK);
You can impose a condition on constraints with ConstraintCondition
interface:
Validator<User> validator = ValidatorBuilder.of(User.class) //
.constraintOnCondition((user, constraintGroup) -> !user.getName().isEmpty(), //
b -> b.constraint(User::getEmail, "email",
c -> c.email().notEmpty())) // <- this constraint on email is active only when name is not empty
.build();
You can group the constraint as a part of ConstraintCondition
with ConstraintGroup
aas well:
enum Group implements ConstraintGroup {
CREATE, UPDATE, DELETE
}
Validator<User> validator = ValidatorBuilder.of(User.class) //
.constraintOnCondition(Group.UPDATE.toCondition(), //
b -> b.constraint(User::getEmail, "email", c -> c.email().notEmpty()))
.build();
The group to validate is specified in validate
method:
validator.validate(user, Group.UPDATE);
You can also use a shortcut constraintOnGroup
method
Validator<User> validator = ValidatorBuilder.of(User.class) //
.constraintOnGroup(Group.UPDATE, //
b -> b.constraint(User::getEmail, "email", c -> c.email().notEmpty()))
.build();
Note that all constraints without conditions will be validated for any constraint group.
By default, some Emojis are not counted as you expect.
For example,
Validator<Message> validator = ValidatorBuilder.<Message> of() //
.constraint(Message::getText, "text", c -> c.notBlank() //
.lessThanOrEqual(3)) //
.build(); //
validator.validate(new Message("I❤️☕️")).isValid(); // false
If you want to count as you see (3, in this case), use emoji()
.
Validator<Message> validator = ValidatorBuilder.<Message> of() //
.constraint(Message::getText, "text", c -> c.notBlank() //
.emoji().lessThanOrEqual(3)) //
.build(); //
validator.validate(new Message("I❤️☕️")).isValid(); // true
For the safety (such as storing into a database), you can also check the size as byte arrays
Validator<Message> validator = ValidatorBuilder.<Message> of() //
.constraint(Message::getText, "text", c -> c.notBlank() //
.emoji().lessThanOrEqual(3)
.asByteArray().lessThanOrEqual(16)) //
.build(); //
validator.validate(new Message("I❤️☕️")).isValid(); // true
validator.validate(new Message("❤️️❤️️❤️️")).isValid(); // false
YAVI will be a great fit for Spring WebFlux.fn
static RouterFunction<ServerResponse> routes() {
return route()
.POST("/", req -> req.bodyToMono(User.class) //
.flatMap(body -> validator.validateToEither(body) //
.leftMap(violations -> {
Map<String, Object> error = new LinkedHashMap<>();
error.put("message", "Invalid request body");
error.put("details", violations.details());
return error;
})
.fold(error -> badRequest().bodyValue(error), //
user -> ok().bodyValue(user))))
.build();
}
@PostMapping("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:/";
}
or
@PostMapping("users")
public String createUser(Model model, UserForm userForm, BindingResult bindingResult) {
return validator.validateToEither(userForm)
.fold(violations -> {
violations.apply(BindingResult::rejectValue);
return "userForm";
}, form -> {
// ...
return "redirect:/";
});
}
- Java 8+
Licensed under the Apache License, Version 2.0.