Validated<E, A>
Runnable example:
ValidatedSample.java
What is Validated<E, A>?
Validated<E, A> models a validation result that either succeeds with a value
of type A or fails with an error of type E.
It is a sealed interface with two implementations:
Valid<E, A>— holds a non-null success value.Invalid<E, A>— holds a non-null error.
The defining feature of Validated is error accumulation: unlike Result,
which stops at the first error (fail-fast), Validated is designed to run all
independent validations and collect every error before returning.
This makes it the right choice for form validation, config parsing, and any
scenario where callers need a complete list of problems, not just the first one.
Use Validated when you need all errors reported at once.
Use Result when the first error is enough and further computation is meaningless.
Creating instances
// Successful resultValidated<String, Integer> valid = Validated.valid(42);
// Failed result — the error type is explicitValidated<String, Integer> invalid = Validated.invalid("must be positive");
// NEL convenience: wraps the error in a singleton NonEmptyList// Use this when combining multiple validations so errors accumulate naturallyValidated<NonEmptyList<String>, Integer> nelInvalid = Validated.invalidNel("must be positive");Both Valid and Invalid reject null. The invalidNel factory is the
preferred starting point when you plan to combine multiple validations — it
wraps the error in a NonEmptyList so errors can be merged with
NonEmptyList::concat. See the NonEmptyList pattern section.
Checking state
Validated<String, Integer> v = Validated.valid(42);
v.isValid(); // truev.isInvalid(); // false
// Exhaustive pattern matching — preferredString msg = switch (v) { case Validated.Valid<String, Integer> ok -> "Value: " + ok.value(); case Validated.Invalid<String, Integer> err -> "Error: " + err.error();};Prefer exhaustive switch patterns — the compiler enforces that both tracks are handled.
Extracting values
| Method | When Invalid | Notes |
|---|---|---|
get() | Throws NoSuchElementException | Only safe after an isValid() check; prefer alternatives. |
getError() | N/A — throws if Valid | Retrieves the error; unsafe on the valid track. |
getOrElse(fallback) | Returns fallback | Fallback is always evaluated eagerly. |
getOrElseGet(supplier) | Calls supplier | Lazily computed fallback. |
getOrNull() | Returns null | Bridge to null-expecting APIs. |
getOrThrow(exMapper) | Throws mapped RuntimeException | Maps the error to an exception at the call site. |
fold(onValid, onInvalid) | Calls onInvalid | The most expressive extractor: forces both tracks to be handled. |
Validated<String, Integer> v = parse(input);
// fold — the most expressive extractor; forces both branchesString rendered = v.fold( value -> "Parsed: " + value, error -> "Invalid: " + error);
// Safe fallbacks (inherited from Bicontainer)int n = v.getOrElse(0);int m = v.getOrElseGet(() -> computeDefault());
// Unsafe accessors — throw NoSuchElementException on the wrong trackint val = v.get(); // throws if InvalidString e = v.getError(); // throws if Valid
// Rethrow as RuntimeException with a custom mapperint forced = v.getOrThrow(err -> new IllegalArgumentException(err));Transforming values
Validated<String, Integer> v = parse(input);
// map — transforms the value; Invalid passes throughValidated<String, String> hex = v.map(Integer::toHexString);
// mapError — transforms the error; Valid passes throughValidated<AppError, Integer> typed = v.mapError(msg -> new AppError(msg));
// flatMap — fail-fast sequential composition (does NOT accumulate)// Use only when the second validation depends on the firstValidated<String, Address> address = validateCity(input) .flatMap(city -> validateZip(input, city)); // zip depends on cityflatMap is available but applies fail-fast semantics — it does not
accumulate errors. Use it only when the second validation logically depends on
the first. For independent validations, always use combine.
Combining independent validations
combine(other, errMerge, valueMerge) is the core of error accumulation.
Both this and other are always evaluated. When both are Invalid, their
errors are merged by errMerge. When both are Valid, the values are merged
by valueMerge.
Argument order — easy to swap accidentally:
combine(other, errMerge, valueMerge) ^^^ ^^^^^^^^^ ^^^^^^^^^^^ 1st 2nd 3rdValidated<List<String>, String> nameV = validateName(req.name());Validated<List<String>, Integer> ageV = validateAge(req.age());Validated<List<String>, String> emailV = validateEmail(req.email());
// combine(other, errMerge, valueMerge)// Argument order: other → errMerge → valueMergeValidated<List<String>, UserForm> form = nameV.combine( ageV, (e1, e2) -> { var merged = new ArrayList<>(e1); merged.addAll(e2); return merged; }, (name, age) -> new UserForm(name, age) ) .combine( emailV, (e1, e2) -> { var merged = new ArrayList<>(e1); merged.addAll(e2); return merged; }, (partial, email) -> partial.withEmail(email) );
// With NonEmptyList error type the merge is just NonEmptyList::concatValidated<NonEmptyList<String>, UserForm> nel = Validated.invalidNel("name required") .combine( Validated.invalidNel("age must be positive"), NonEmptyList::concat, (name, age) -> new UserForm(name, age) );// nel.getError() → NonEmptyList["name required", "age must be positive"]product(other, errMerge) is the lower-level variant that returns
Validated<E, Tuple2<A, B>> instead of applying a value merger.
Use combine in most cases.
Combining three or four validations
For three or four independent fields, combine3 / combine4 collapse the chain
into a single flat call — no intermediate Tuple2 nesting, no manual unpacking.
| Method | Inputs | Value merger |
|---|---|---|
combine(other, errMerge, valueMerge) | 2 | BiFunction<A, B, R> |
Validated.combine3(va, vb, vc, errMerge, valueMerge) | 3 | TriFunction<A, B, C, R> |
Validated.combine4(va, vb, vc, vd, errMerge, valueMerge) | 4 | QuadFunction<A, B, C, D, R> |
All inputs are always evaluated — no short-circuit. All errors are accumulated.
// combine3 — three independent validations in one flat callValidated<NonEmptyList<String>, RegistrationForm> form3 = Validated.combine3( validateUsername(username), validateEmail(email), validateAge(age), NonEmptyList::concat, RegistrationForm::new // (String, String, Integer) -> RegistrationForm);
// combine4 — four independent validations in one flat callValidated<NonEmptyList<String>, UserProfile> profile = Validated.combine4( validateUsername(username), validateEmail(email), validateAge(age), validateCountry(country), NonEmptyList::concat, UserProfile::new // (String, String, Integer, String) -> UserProfile);
// All errors accumulated — no short-circuitValidated<NonEmptyList<String>, RegistrationForm> allInvalid = Validated.combine3( Validated.invalidNel("username too short"), Validated.invalidNel("missing @"), Validated.invalidNel("age must be positive"), NonEmptyList::concat, RegistrationForm::new);// Invalid(["username too short", "missing @", "age must be positive"])The NonEmptyList (NEL) pattern
Using NonEmptyList<E> as the error type is the idiomatic way to accumulate
multiple errors. The invalidNel, sequenceNel, and traverseNel factory
methods eliminate the need to pass NonEmptyList::concat manually.
// invalidNel wraps a single error in a NonEmptyList — errors accumulate via NonEmptyList::concatValidated<NonEmptyList<String>, String> nameV = Validated.invalidNel("name is required");Validated<NonEmptyList<String>, Integer> ageV = Validated.invalidNel("age must be positive");Validated<NonEmptyList<String>, String> emailV = Validated.valid("user@example.com");
Validated<NonEmptyList<String>, RegistrationForm> result = nameV.combine(ageV, NonEmptyList::concat, (n, a) -> new PartialForm(n, a)) .combine(emailV, NonEmptyList::concat, (pf, em) -> new RegistrationForm(pf, em));
// result.getError() → NonEmptyList["name is required", "age must be positive"]
// sequenceNel and traverseNel are shorthand — no need to pass the mergerList<String> rawEmails = List.of("a@b.com", "bad-email", "c@d.com", "also-bad");
Validated<NonEmptyList<String>, List<String>> emails = Validated.traverseNel(rawEmails, email -> validateEmail(email));// → Invalid(NonEmptyList["bad-email: invalid format", "also-bad: invalid format"])Accumulating over collections
sequence and traverse run all elements — they do not short-circuit on
the first Invalid. All errors are merged left-to-right using errMerge.
List<Validated<String, Integer>> items = List.of( Validated.valid(1), Validated.invalid("too small"), Validated.valid(3), Validated.invalid("too large"));
// sequence: accumulates ALL errors — does not short-circuitBinaryOperator<String> joinErrors = (a, b) -> a + "; " + b;
Validated<String, List<Integer>> result = Validated.sequence(items, joinErrors);// → Invalid("too small; too large")
// traverse: map + accumulate in one stepValidated<String, List<Integer>> parsed = Validated.traverse( List.of("1", "bad", "3", "worse"), s -> parsePositive(s), joinErrors );// → Invalid("bad: not a number; worse: not a number")
// As a Stream Collector — composable with other stream operationsValidated<String, List<Integer>> collected = items.stream().collect(Validated.collector(joinErrors));A Collector variant is available via Validated.collector(errMerge) and
Validated.traverseCollector(mapper, errMerge), compatible with parallel
streams and the standard Stream.collect API.
Interoperability
| Conversion | Method |
|---|---|
Validated → Result | v.toResult() |
Validated → Try | v.toTry(errorMapper) |
Validated → Either | v.toEither() |
Validated → Option | v.toOption() |
Result → Validated | Validated.fromResult(result) |
Option → Validated | Validated.fromOption(option, errorIfNone) |
Try → Validated | Validated.fromTry(t, errorMapper) |
Validated<String, Integer> v = parseAge(input);
// Validated → Result (Valid→Ok, Invalid→Err)Result<Integer, String> result = v.toResult();
// Validated → Try (requires mapping the error to a Throwable)Try<Integer> t = v.toTry(err -> new IllegalArgumentException(err));
// Validated → Either (Valid→right, Invalid→left)Either<String, Integer> either = v.toEither();
// Validated → Option (discards the error)Option<Integer> opt = v.toOption();
// Result → ValidatedValidated<String, Integer> fromResult = Validated.fromResult(result);
// Option → Validated (supply error for the None case)Validated<String, Integer> fromOption = Validated.fromOption(opt, "value missing");
// Try → Validated (map exception to error type)Validated<String, Integer> fromTry = Validated.fromTry(t, Throwable::getMessage);See the Combining Types page for the full conversion matrix.
Validated vs Result — when to use each
| Scenario | Use |
|---|---|
| Stop at the first error; further steps depend on success | Result |
| Collect all errors; report everything at once | Validated |
| Wrap throwing legacy code | Try |
| Independent field validations (form, config) | Validated |
| Pipeline where each step feeds the next | Result |
Common pitfalls
Wrong argument order in combine
errMerge is the second argument; valueMerge is the third.
Swapping them is a common mistake because both are BiFunction-like arguments.
// combine(other, errMerge, valueMerge)// ^^^ ^^^^^^^^^ ^^^^^^^^^^^// 1st 2nd 3rd
// Bad: arguments in the wrong order — does not compile or produces wrong resultv1.combine(v2, (a, b) -> new Form(a, b), NonEmptyList::concat); // ← swapped!
// Good: errMerge is ALWAYS the second argument, valueMerge is ALWAYS thirdv1.combine(v2, NonEmptyList::concat, (a, b) -> new Form(a, b));Using flatMap instead of combine for independent validations
flatMap short-circuits — it stops at the first Invalid.
Use combine when the validations are independent and all errors should be reported.
// Bad: flatMap does NOT accumulate — it short-circuits on the first InvalidValidated<String, Form> form = validateName(req.name()) .flatMap(name -> validateAge(req.age())) // stops here if name is invalid .flatMap(age -> validateEmail(req.email())); // never reached if name or age fails
// Good: use combine for independent validations so all errors are collectedValidated<NonEmptyList<String>, Form> form = Validated.invalidNel(validateName(req.name()).getError()) .combine(Validated.invalidNel(validateAge(req.age()).getError()), NonEmptyList::concat, (name, age) -> new Form(name, age));
// flatMap is correct only when the second validation depends on the firstValidated<String, Address> address = validateCountry(req.country()) .flatMap(country -> validatePostalCode(req.postalCode(), country));Real-world example
A user registration form: name, email, age, and password are all validated independently. All errors are accumulated and returned in a single response.
// Each field validation returns Validated<NonEmptyList<String>, T>// All run independently — all errors are collected
Validated<NonEmptyList<String>, String> nameV = Optional.ofNullable(req.name()) .filter(s -> !s.isBlank()) .map(Validated::<NonEmptyList<String>, String>valid) .orElseGet(() -> Validated.invalidNel("name is required"));
Validated<NonEmptyList<String>, String> emailV = Optional.ofNullable(req.email()) .filter(EMAIL_PATTERN.asPredicate()) .map(Validated::<NonEmptyList<String>, String>valid) .orElseGet(() -> Validated.invalidNel("email is invalid"));
Validated<NonEmptyList<String>, Integer> ageV = Try.of(() -> Integer.parseInt(req.age())) .toResult(ex -> NonEmptyList.singleton("age must be a number")) .flatMap(n -> n >= 18 ? Result.ok(n) : Result.err(NonEmptyList.singleton("must be 18 or older"))) .fold(Validated::valid, Validated::invalid);
Validated<NonEmptyList<String>, String> passwordV = Optional.ofNullable(req.password()) .filter(p -> p.length() >= 8) .map(Validated::<NonEmptyList<String>, String>valid) .orElseGet(() -> Validated.invalidNel("password must be at least 8 characters"));
// Combine all — all fields are evaluated; all errors are accumulatedValidated<NonEmptyList<String>, RegistrationForm> form = nameV .combine(emailV, NonEmptyList::concat, (n, e) -> new PartialForm(n, e)) .combine(ageV, NonEmptyList::concat, (pf, a) -> pf.withAge(a)) .combine(passwordV, NonEmptyList::concat, (pf, pwd) -> new RegistrationForm(pf, pwd));
// At the call siteform.fold( registration -> userService.register(registration), errors -> Response.unprocessable(errors.toList()));