Validation¶
Validation<E, A> is Valid(A value) or Invalid(NonEmptyVector<E> errors). Either stops at the first
failure; combining validations keeps the errors of every one of them, in order.
The errors are held in a NonEmptyVector, so an Invalid always carries at least one.
When to use it¶
Use Validation for independent checks whose errors should all be reported at once: the fields of a form, the
entries of a configuration file.
- If each step needs the result of the previous one, the first error stops the work anyway: use
Either. - If the error is an exception thrown by the code you call, use
Try, orValidation.ofbelow.
Checks¶
A check returns a Validation. fromPredicate builds one from a test, and from a function that turns the rejected
value into an error message.
static Validation<String, String> name(String value) {
return value.isBlank() ? Validation.invalid("name is blank") : Validation.valid(value);
}
static Validation<String, Integer> age(int value) {
return Validation.fromPredicate(value, v -> v >= 0, v -> "age is negative");
}
static Validation<String, String> email(String value) {
return Validation.fromPredicate(value, v -> v.contains("@"), v -> "email has no @");
}
fromEither, fromOption and fromTry convert the other control types. Validation.of runs code that may throw
and turns the exception into an error.
var port = Validation.of(() -> Integer.parseInt("80a"),
e -> "port is not a number"); // Validation<String, Integer>
// Invalid(port is not a number)
Accumulation with zip and zipWith¶
zipWith combines independent checks and calls the function only when all of them are valid. The static form
takes 2 to 8 checks (zip at arity N); the instance form combines two.
record User(String name, int age, String email) {}
var user = Validation.zipWith(name(""), age(-1), email("jules"), User::new); // Validation<String, User>
// Invalid(name is blank, age is negative, email has no @)
var pair = name("Ada").zip(age(36)); // Validation<String, Tuple2<String, Integer>>
var both = name("").zipWith(age(-1), (n, a) -> n + a); // Validation<String, String>
// Valid((Ada, 36)), Invalid(name is blank, age is negative)
zip(Either) treats a Left as one more error: Invalid(a) zipped with Left(b) is Invalid(a, b).
Reading the result¶
Pattern match on the records, or fold with the errors first:
var checked = age(-5); // Validation<String, Integer>
var message = switch (checked) {
case Valid(var years) -> "age " + years;
case Invalid(var errors) -> errors.size() + " error(s): " + errors.mkString("; ");
};
// "1 error(s): age is negative"
toEither() gives an Either<NonEmptyVector<E>, A>, for code that expects one.
Many values: collectAll, forEach, partition¶
collectAll turns many validations into one validation of a Vector, keeping every error. forEach does the
same after running a check on each input.
var ages = Validation.forEach(Vector.of(3, -1, 7, -2),
n -> age(n)); // Validation<String, Vector<Integer>>
// Invalid(age is negative, age is negative)
var names = Validation.collectAll(
Vector.of(name("Ada"), name("Grace"))); // Validation<String, Vector<String>>
// Valid(Vector(Ada, Grace))
forEach over a NonEmptyVector returns a NonEmptyVector on the valid side too.
var scores = Validation.forEach(NonEmptyVector.of(1, 2),
n -> age(n)); // Validation<String, NonEmptyVector<Integer>>
// Valid(NonEmptyVector(1, 2))
partition never fails: it returns the errors and the successes side by side.
var split = Validation.partition(Vector.of(4, -1, 9),
n -> age(n)); // Tuple2<Vector<String>, Vector<Integer>>
// (Vector(age is negative), Vector(4, 9))
Short-circuiting on purpose: flatMap¶
flatMap runs the next check only when this one is valid, so the two checks' errors are never combined. Use it
for a rule that needs the valid value, after the independent checks:
var adult = Validation.zipWith(name("Ada"), age(15), email("ada@example.com"), User::new)
.flatMapEither(u -> u.age() >= 18 ? Either.right(u) : Either.left(u.name() + " is under 18"));
// Invalid(Ada is under 18)
flatMapEither is the same for a step that returns an Either. If every step depends on the previous one, Either
is the simpler type.
Transforming the errors¶
mapError transforms each error; mapErrorAll transforms the NonEmptyVector of errors as a whole, and the result
stays non-empty. mapBoth transforms the errors and the value at once.
Conversions¶
toEither()gives anEither<NonEmptyVector<E>, A>;toEitherWith(Function)builds the left value from the errors.toTry(Function)builds theFailurecause from the errors.toOption()andtoVector()keep the value and drop the errors.
var negative = age(-3); // Validation<String, Integer>
var joined = negative.toEitherWith(errors -> errors.mkString("; ")); // Either<String, Integer>
var failure = negative.toTry(
errors -> new IllegalArgumentException(errors.mkString("; "))); // Try<Integer>
// Left(age is negative), Failure(java.lang.IllegalArgumentException: age is negative)
Sharp edges¶
orElsedoes not combine errors: when both sides are invalid, it returns the second one and drops the first one's errors.- Equality depends on the order of the errors:
Invalid(a, b)is not equal toInvalid(b, a). - There is no
flip: the two sides are not alike, since one of them is never empty. - Neither case holds
null:valid(null)andinvalid(null)throw aNullPointerException.