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 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"])

Both factory overloads throw NullPointerException for null predicate or message arguments.

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)

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 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"])

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
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 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")
// 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"]

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.