Guard<T>
Runnable example:
GuardSample.java
What is Guard<T>?
Guard<T> is a @FunctionalInterface that wraps a single validation rule:
a predicate over T with an associated error message.
Applying a guard via check(value) returns a
Validated<NonEmptyList<String>, T> — Valid(value) when the rule passes,
or Invalid(errors) when it fails.
Guards are the composable building block for Validated pipelines.
Instead of writing repeated if/Validated.invalidNel(...) branches, define each
rule once as a Guard and compose them with and, or, and negate.
Creating guards
// Static error messageGuard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");Guard<String> minLength3 = Guard.of(s -> s.length() >= 3, "must be at least 3 chars");
// Dynamic error message — the failing value is passed to the functionGuard<Integer> max = Guard.of( n -> n <= 100, n -> "must be ≤ 100, got " + n);
// ApplynotBlank.check("hello"); // Valid("hello")notBlank.check(" "); // Invalid(["must not be blank"])max.check(150); // Invalid(["must be ≤ 100, got 150"])Both factory overloads throw NullPointerException for null predicate or message arguments.
and(other) — accumulate all errors
Both guards are always evaluated — and is not fail-fast.
Errors from all failing guards are accumulated into a single NonEmptyList so the caller
receives a complete picture of every violation.
Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");Guard<String> minLength3 = Guard.of(s -> s.length() >= 3, "min 3 chars");Guard<String> alphanumeric = Guard.of(s -> s.matches("[a-zA-Z0-9]+"), "must be alphanumeric");
// Both guards are always evaluated — errors accumulate (not fail-fast)Guard<String> username = notBlank.and(minLength3).and(alphanumeric);
username.check("alice"); // Valid("alice")username.check("a!"); // Invalid(["min 3 chars", "must be alphanumeric"])username.check(" "); // Invalid(["must not be blank", "min 3 chars", "must be alphanumeric"])or(other) — pass on first success
Evaluation is short-circuit: if the left guard passes, the right guard is never evaluated. If all guards fail, errors from all of them are accumulated.
Guard<String> email = Guard.of(s -> s.contains("@"), "must contain @");Guard<String> phone = Guard.of(s -> s.matches("\\d+"), "must be all digits");
// First passing guard short-circuits; all errors accumulated on total failureGuard<String> contact = email.or(phone);
contact.check("alice@example.com"); // Valid — email passes, phone not evaluatedcontact.check("12345"); // Valid — phone passescontact.check("hello"); // Invalid(["must contain @", "must be all digits"])Use or when a value is allowed to satisfy any one of several alternative formats
(e.g., email or phone number, UUID or slug).
negate() / negate(message)
Inverts the predicate. Use negate(message) to supply a domain-specific error
message; negate() falls back to the generic message "must not satisfy the condition".
// negate(message) — inverts the predicate with a domain-specific errorGuard<String> noAdmin = Guard.<String>of(s -> s.equals("admin"), "reserved") .negate("username must not be 'admin'");
noAdmin.check("alice"); // Valid("alice")noAdmin.check("admin"); // Invalid(["username must not be 'admin'"])
// negate() with no argument uses a generic messageGuard<String> notReserved = Guard.<String>of(s -> s.equals("root"), "is root").negate();notReserved.check("root"); // Invalid(["must not satisfy the condition"])Integrating with Validated.combine
Guards produce Validated<NonEmptyList<String>, T> values, which compose naturally with
Validated.combine. Apply one guard per field, then combine
them to accumulate errors across an entire form or command object:
Validated<NonEmptyList<String>, String> username = usernameGuard.check(rawUsername);Validated<NonEmptyList<String>, String> email = emailGuard.check(rawEmail);
Validated<NonEmptyList<String>, Form> form = username.combine(email, NonEmptyList::concat, Form::new);// Invalid(["min 3 chars", "must be alphanumeric", "invalid email format"])Interoperability
| Method | Returns | Use case |
|---|---|---|
asPredicate() | Predicate<T> | Stream.filter, Collection.removeIf, any standard Java predicate API |
contramap(fn) | Guard<U> | Adapt a field guard to validate the enclosing object type |
checkToResult(value) | Result<T, NonEmptyList<String>> | Direct integration into Result-based domain logic |
checkToResult(value, mapper) | Result<T, E> | Map errors to a domain-specific error type at service boundaries |
checkToOption(value) | Option<T> | Discard errors — keep only valid values (stream filtering) |
Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");Guard<String> minLength3 = Guard.of(s -> s.length() >= 3, "min 3 chars");Guard<String> username = notBlank.and(minLength3);
// asPredicate() — Java stdlib interopList<String> validNames = Stream.of("alice", " ", "bob") .filter(notBlank.asPredicate()) // standard Predicate<String> .toList();// ["alice", "bob"]
// contramap(fn) — adapt a field guard to an enclosing typerecord User(String name) {}Guard<User> userGuard = notBlank.contramap(User::name);userGuard.check(new User("alice")); // Valid(User[name=alice])userGuard.check(new User(" ")); // Invalid(["must not be blank"])
// checkToResult(value) — direct Result<T, NonEmptyList<String>>Result<String, NonEmptyList<String>> r1 = username.checkToResult("alice");// Result.ok("alice")
// checkToResult(value, mapper) — map errors to a domain typeResult<String, String> r2 = username.checkToResult( "a!", errors -> String.join("; ", errors.toList()));// Result.err("min 3 chars")
// checkToOption(value) — discard errors, keep value or NoneOption<String> opt = username.checkToOption("alice"); // Some("alice")username.checkToOption("a!"); // None
// flatMap over a stream, keeping only valid valuesList<String> valid = Stream.of("alice", "a!", "bob") .flatMap(s -> username.checkToOption(s).stream()) .toList();// ["alice", "bob"]Real-world example
A registration form validator that composes rules across three fields:
// ---- Define reusable rules once ----
Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");Guard<String> minLength3 = Guard.of(s -> s.length() >= 3, "min 3 chars");Guard<String> maxLength50 = Guard.of(s -> s.length() <= 50, "max 50 chars");Guard<String> alphanumeric = Guard.of(s -> s.matches("[a-zA-Z0-9_]+"), "must be alphanumeric or _");Guard<String> emailFormat = Guard.of(s -> s.contains("@") && s.contains("."), "invalid email format");Guard<Integer> adultAge = Guard.of(n -> n >= 18, "must be at least 18");
// ---- Compose into domain rules ----
Guard<String> usernameGuard = notBlank.and(minLength3).and(maxLength50).and(alphanumeric);Guard<String> emailGuard = notBlank.and(emailFormat);
// ---- Validate all fields and accumulate errors ----
static Validated<NonEmptyList<String>, RegistrationForm> validate( String rawUsername, String rawEmail, int age) {
Validated<NonEmptyList<String>, String> username = usernameGuard.check(rawUsername); Validated<NonEmptyList<String>, String> email = emailGuard.check(rawEmail); Validated<NonEmptyList<String>, Integer> ageV = adultAge.check(age);
return username .combine(email, NonEmptyList::concat, Tuple2::new) .combine(ageV, NonEmptyList::concat, (ue, a) -> new RegistrationForm(ue._1(), ue._2(), a));}
// validate("al", "not-an-email", 15)// → Invalid(["min 3 chars", "invalid email format", "must be at least 18"])Guard vs raw Validated
Raw Validated.invalidNel | Guard<T> | |
|---|---|---|
| Rule definition | Inline if/invalidNel | Named value — defined once, used anywhere |
| Reuse across call sites | Copy-paste | Reference the same Guard constant |
| Composition | Manual chaining | and, or, negate |
| Error accumulation | Explicit combine calls | Built into and / or |
| Dynamic error messages | Inline string interpolation | Guard.of(pred, value -> "... " + value) |
Use Guard when the same rule appears in more than one place, when rules are composed
from smaller building blocks, or when you want a named, self-documenting validation vocabulary.
For a single, one-off check, a direct Validated.invalidNel is simpler.