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 message
Guard<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 function
Guard<Integer> max = Guard.of(
n -> n <= 100,
n -> "must be ≤ 100, got " + n
);
// Apply
notBlank.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 null
Guard<@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 null
Guard<@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 called
nonNullAndNotBlank.check(" "); // Invalid(["must not be blank"])

and(other) — accumulate all errors

Both guards are always evaluatedand 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 failure
Guard<String> contact = email.or(phone);
contact.check("alice@example.com"); // Valid — email passes, phone not evaluated
contact.check("12345"); // Valid — phone passes
contact.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:

OverloadError 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 error
Guard<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 message
Guard<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 value
Guard<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

MethodReturnsUse 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 interop
List<String> validNames = Stream.of("alice", " ", "bob")
.filter(notBlank.asPredicate()) // standard Predicate<String>
.toList();
// ["alice", "bob"]
// contramap(fn) — adapt a field guard to an enclosing type
record 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 type
Result<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 IllegalArgumentException
Try<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 type
Try<String> typed = username.checkToTry(
"a!",
errors -> new ValidationException(errors.toList())
);
// Try.failure(new ValidationException(["min 3 chars"]))
// checkToOption(value) — discard errors, keep value or None
Option<String> opt = username.checkToOption("alice"); // Some("alice")
username.checkToOption("a!"); // None
// flatMap over a stream, keeping only valid values
List<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 chaining
String upper = username.checkToOptional(input)
.map(String::toUpperCase)
.orElse("INVALID");
// withMessage(message) — replace all errors with a single fixed message
Guard<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.invalidNelGuard<T>
Rule definitionInline if/invalidNelNamed value — defined once, used anywhere
Reuse across call sitesCopy-pasteReference the same Guard constant
CompositionManual chainingand, or, negate
Error accumulationExplicit combine callsBuilt into and / or
Dynamic error messagesInline string interpolationGuard.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.