Validly provides a clean and convenient way for implementing validation logic by providing a fluent API and abstracting away the conditional constructs of the language.
Validly allows you to:
- Focus on expressing the validation rules in a declarative way instead of writing an if-else mess.
- Use Notification pattern to report all the validation errors.
- Write your own domain specific validation language by extending Validly.
<dependency>
<groupId>io.validly</groupId>
<artifactId>Validly</artifactId>
<version>0.1.0</version>
</dependency>
import static io.validly.FailFastValidator.*;
public class Validator {
public void validate(String input) throws ValidationErrorException {
valid(input)
.mustNotBeNull("It's null")
.lengthMustBeAtLeast(2, "It's too short")
.mustStartWith("Hello", "It doesn't start properly")
.must(s -> myOwnRule(s), "It doesn't match my own rule");
}
}
Validation rule is defined by passing the value to the valid-method and providing a set of Predicates. The library provides predefined predicates (mustNotBeNull, lengthMustBeAtLesast etc.) but custom predicates can be used in with the must-method.
Validly has three different validation modes.
- Fail-Fast: throws a ValidationErrorException when validation error occurs. (Example above)
- Note-First: gathers the first error of each value into a List or Notification object.
- Note-All: gathers all the errors of each value into a List or Notification object.
Fail-Fast mode is ideal when validating one input value. Replacing throwing exceptions with notification makes sense if you want to report more than just the first occurring validation error - ideal when validating domain objects:
// If you want to report every error of each field use NoteAllValidator
import static io.validly.NoteFirstValidator.*;
public class CustomerScenario {
public Notification validate(Customer customer) {
Notification note = new Notification(); // You can also use List
valid(customer.getName(), "name", note)
.mustNotBeBlank("Can't be blank")
.lengthMustNotExceed(20, "Too long");
valid(customer.getAge(), "age", note)
.mustNotBeNull("Can't be null")
.valueMustBeAtLeast(0, "Can't be negative");
valid(customer.getSsn(), "ssn", note)
.mustNotBeNullWhen(customer.getAge() >= 18, "Required for adults")
.must(s -> s.matches("//your.regex+"), "Invalid format");
return note;
}
}
Build a conditional validation with when-method by providing a boolean and at least one Then predicate. Then will only be evaluated if the preceding boolean is true.
valid(address.getState(), note)
.when(countryRequiresState(address),
Then.mustNotBeNull("Is required"),
Then.must(validStateForCountry(address), "Invalid value"));
Convert the input type by providing a Function to mustConvert-method. The new type is usable in the subsequent predicates. If the provided conversion Function throws an Exception or returns a null value, a validation error occurs and the subsequent predicates will not be evaluated (even in Note-All mode).
valid(address.getMoveInDate(), note)
.canBeNull()
.mustConvert(s -> LocalDate.parse(s, ofPattern("dd.MM.yyyy")), "Invalid value")
.must(d -> d.isBefore(LocalDate.now()), "Must be in the past");
Validation engine always infers the type of the provided input. In this case the type is List<String>:
valid(address.getAddressLines(), note)
.mustNotBeNull("Is required")
.must(lines -> lines.size() >= 2, "min two required") // input type is usable here
.must(lines -> lines.stream().allMatch(s -> s.length() < 100), "must be under 100 chars");
Create a clean DSL for your validation by naming the custom predicates to be in line with the Validly's must-convention:
import static io.validly.NoteAllValidator.valid;
public class PasswordScenario {
public List<String> validate(PasswordChangeRequest passwordChangeRequest) {
List<String> note = new ArrayList<>();
valid(passwordChangeRequest.getNewPassword(), note)
.mustNotBeBlank("Is null or empty")
.lengthMustBeAtLeast(6, "Too short")
.lengthMustNotExceed(24, "Too long")
.must(notContain(" "), "Contains space")
.must(containDigits() // Using Predicate's and-method to compose two predicates
.and(containLetters()), "Doesn't contain numbers and letters")
.must(notBlacklisted(), "Blacklisted value");
valid(passwordChangeRequest.getCurrentPassword(), note)
.must(equalActualCurrentPassword(), "Invalid current password");
return note;
}
private Predicate<String> notBlacklisted() {
return BLACKLIST::contains;
}
private Predicate<String> containDigits() {
return s -> s.matches(".*\\d.*");
}
private Predicate<String> containLetters() {
return s -> s.matches(".*[a-zA-Z]+.*");
}
private Predicate<String> notContain(CharSequence... value) {
return s -> !Arrays.asList(value).contains(s);
}
private Predicate<String> equalActualCurrentPassword() {
return customerService::passwordEquals;
}
}