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 result
Validated<String, Integer> valid = Validated.valid(42);
// Failed result — the error type is explicit
Validated<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 naturally
Validated<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(); // true
v.isInvalid(); // false
// Exhaustive pattern matching — preferred
String 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

MethodWhen InvalidNotes
get()Throws NoSuchElementExceptionOnly safe after an isValid() check; prefer alternatives.
getError()N/A — throws if ValidRetrieves the error; unsafe on the valid track.
getOrElse(fallback)Returns fallbackFallback is always evaluated eagerly.
getOrElseGet(supplier)Calls supplierLazily computed fallback.
getOrNull()Returns nullBridge to null-expecting APIs.
getOrThrow(exMapper)Throws mapped RuntimeExceptionMaps the error to an exception at the call site.
fold(onValid, onInvalid)Calls onInvalidThe most expressive extractor: forces both tracks to be handled.
Validated<String, Integer> v = parse(input);
// fold — the most expressive extractor; forces both branches
String 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 track
int val = v.get(); // throws if Invalid
String e = v.getError(); // throws if Valid
// Rethrow as RuntimeException with a custom mapper
int forced = v.getOrThrow(err -> new IllegalArgumentException(err));

Transforming values

Validated<String, Integer> v = parse(input);
// map — transforms the value; Invalid passes through
Validated<String, String> hex = v.map(Integer::toHexString);
// mapError — transforms the error; Valid passes through
Validated<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 first
Validated<String, Address> address = validateCity(input)
.flatMap(city -> validateZip(input, city)); // zip depends on city

flatMap 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 3rd
Validated<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 → valueMerge
Validated<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::concat
Validated<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.

MethodInputsValue merger
combine(other, errMerge, valueMerge)2BiFunction<A, B, R>
Validated.combine3(va, vb, vc, errMerge, valueMerge)3TriFunction<A, B, C, R>
Validated.combine4(va, vb, vc, vd, errMerge, valueMerge)4QuadFunction<A, B, C, D, R>

All inputs are always evaluated — no short-circuit. All errors are accumulated.

// combine3 — three independent validations in one flat call
Validated<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 call
Validated<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-circuit
Validated<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::concat
Validated<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 merger
List<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-circuit
BinaryOperator<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 step
Validated<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 operations
Validated<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

ConversionMethod
ValidatedResultv.toResult()
ValidatedTryv.toTry(errorMapper)
ValidatedEitherv.toEither()
ValidatedOptionv.toOption()
ResultValidatedValidated.fromResult(result)
OptionValidatedValidated.fromOption(option, errorIfNone)
TryValidatedValidated.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 → Validated
Validated<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

ScenarioUse
Stop at the first error; further steps depend on successResult
Collect all errors; report everything at onceValidated
Wrap throwing legacy codeTry
Independent field validations (form, config)Validated
Pipeline where each step feeds the nextResult

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 result
v1.combine(v2, (a, b) -> new Form(a, b), NonEmptyList::concat); // ← swapped!
// Good: errMerge is ALWAYS the second argument, valueMerge is ALWAYS third
v1.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 Invalid
Validated<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 collected
Validated<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 first
Validated<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 accumulated
Validated<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 site
form.fold(
registration -> userService.register(registration),
errors -> Response.unprocessable(errors.toList())
);