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.
All composition operators (and, or, negate, andThen, contramap) are default methods
on the interface, so guards can be defined as lambdas and composed without inheritance.
This design decision is documented in ADR-011 — Guard<T> as a @FunctionalInterface with default methods.
The error type is fixed as NonEmptyList<String> — this keeps composition simple (no external
Monoid<E> needed for and/or) and guarantees at least one error is always present. This
design decision is documented in ADR-005 — Guard<T> accumulates errors as a fixed NonEmptyList<String>.
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"])
// Built-in: nonNull() — accepts @Nullable T, rejects nullGuard<@Nullable String> nonNull = Guard.nonNull();nonNull.check("hello"); // Valid("hello")nonNull.check(null); // Invalid(["must not be null"])
// Compose nonNull with further rules using andThen() — short-circuits on Invalid// so the RHS predicate never receives nullGuard<@Nullable String> nonNullAndNotBlank = Guard.<@Nullable String>nonNull() .andThen(Guard.<@Nullable String>of(s -> s != null && !s.isBlank(), "must not be blank"));Both factory overloads throw NullPointerException for null predicate or message arguments.
Built-in guards
Guard.nonNull()
Returns a Guard<@Nullable T> that accepts a nullable input and produces
Invalid(["must not be null"]) when the value is null, or Valid(value) otherwise.
The type parameter is declared <@Nullable T> so the compiler knows that passing null
is intentional — no @SuppressWarnings needed:
Guard<@Nullable String> nonNull = Guard.nonNull();nonNull.check("hello"); // Valid("hello")nonNull.check(null); // Invalid(["must not be null"])To chain nonNull() with further rules, use andThen() rather than and().
and() always evaluates both sides, so a downstream Guard.of(s -> !s.isBlank(), ...)
would receive null and throw when the input is null. andThen() short-circuits:
if this guard returns Invalid, the next guard is never called.
Guard<@Nullable String> nonNullAndNotBlank = Guard.<@Nullable String>nonNull() .andThen(Guard.<@Nullable String>of(s -> s != null && !s.isBlank(), "must not be blank"));
nonNullAndNotBlank.check("hello"); // Valid("hello")nonNullAndNotBlank.check(null); // Invalid(["must not be null"]) — RHS never callednonNullAndNotBlank.check(" "); // Invalid(["must not be blank"])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) / negate(messageFromValue)
Inverts the predicate. Three overloads are available:
| Overload | Error message when original guard passes |
|---|---|
negate() | Generic: "must not satisfy the condition" |
negate(String message) | Fixed string supplied by the caller |
negate(Function<T,String> fn) | Dynamic — the function receives the passing value |
// 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"])
// negate(messageFromValue) — dynamic message from the failing valueGuard<String> notReservedDynamic = Guard.<String>of( s -> s.equals("admin") || s.equals("root"), "reserved" ).negate(s -> "username '" + s + "' is reserved");
notReservedDynamic.check("alice"); // Valid("alice")notReservedDynamic.check("admin"); // Invalid(["username 'admin' is reserved"])notReservedDynamic.check("root"); // Invalid(["username 'root' is reserved"])withMessage(message)
Replaces all error messages produced by this guard with a single fixed message. Useful at public API boundaries where you want to hide internal validation details and expose one clean, user-facing message regardless of which rule failed:
Guard<String> username = notBlank.and(minLength3).and(alphanumeric) .withMessage("invalid username");
username.check(""); // Invalid(["invalid username"])username.check("a"); // Invalid(["invalid username"]) — not "min 3 chars"username.check("alice"); // Valid("alice")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 |
|---|---|---|
withMessage(message) | Guard<T> | Replace all errors with one fixed message (hides internal rule details) |
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 |
checkToEither(value) | Either<NonEmptyList<String>, T> | Integrate with Either-based pipelines (left = errors, right = value) |
checkToTry(value) | Try<T> | Integrate with Try-based pipelines; errors joined as IllegalArgumentException |
checkToTry(value, toThrowable) | Try<T> | Integrate with Try-based pipelines; caller controls the exception type |
checkToOption(value) | Option<T> | Discard errors — keep only valid values (stream filtering) |
checkToOptional(value) | Optional<T> | Java standard Optional interop — drop-in for APIs expecting Optional |
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")
// checkToEither(value) — Either<NonEmptyList<String>, T>Either<NonEmptyList<String>, String> right = username.checkToEither("alice");// Either.right("alice")Either<NonEmptyList<String>, String> left = username.checkToEither("a!");// Either.left(NonEmptyList.of("min 3 chars"))
// checkToTry(value) — Try<T> with default IllegalArgumentExceptionTry<String> success = username.checkToTry("alice");// Try.success("alice")Try<String> failure = username.checkToTry("a!");// Try.failure(new IllegalArgumentException("min 3 chars"))
// checkToTry(value, toThrowable) — caller controls the exception typeTry<String> typed = username.checkToTry( "a!", errors -> new ValidationException(errors.toList()));// Try.failure(new ValidationException(["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"]
// checkToOptional(value) — Java standard Optional<T>Optional<String> present = username.checkToOptional("alice"); // Optional.of("alice")Optional<String> empty = username.checkToOptional("a!"); // Optional.empty()
// integrates naturally with Optional chainingString upper = username.checkToOptional(input) .map(String::toUpperCase) .orElse("INVALID");
// withMessage(message) — replace all errors with a single fixed messageGuard<String> usernameSafe = notBlank.and(minLength3).withMessage("invalid username");usernameSafe.check(""); // Invalid(["invalid username"])usernameSafe.check("a"); // Invalid(["invalid username"])usernameSafe.check("alice"); // Valid("alice")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.