10 — Validation
Built-in Validation Rules
rulii ships with 38 ready-to-use validation rules. Each one checks a specific constraint and adds a RuleViolation when the check fails. The recommended way to create them is through the Validators static factory, introduced in 1.2.0.
The Validators Factory
Validators provides one static factory method per built-in rule. Each method returns a fluent builder so you can customize the error code, severity, and message before calling .build().
Pass a value supplier as the first argument. Use binding("fieldName") to look up a named binding from the RuleContext at execution time, or value(someObject) to supply a constant.
import static org.rulii.validation.rules.Validators.*;
// Simple: validate the "name" binding is not null
Rule notNull = notNull(binding("name")).build();
// With custom error details
Rule notNull = notNull(binding("name"))
.errorCode("ERR-001")
.severity(Severity.FATAL)
.message("Name is required!")
.build();
// Range rule with parameters
Rule sizeCheck = size(binding("name"), 2, 50).build();
// Run it
RuleViolations violations = new RuleViolations();
notNull.run(name -> null, ruleViolations -> violations);
violations.hasErrors(); // true — name was null
Examples
The examples below show the most common patterns: custom error codes and messages, supplying a constant with value(), and rules that accept additional parameters such as bounds or a regex pattern.
import static org.rulii.validation.rules.Validators.*;
// Custom error code and message
Rule usernameRequired = notBlank(binding("username"))
.errorCode("USER-001")
.message("Username must not be blank.")
.build();
// Custom error code with the default message kept (omit .message())
Rule emailValid = email(binding("email"))
.errorCode("USER-002")
.build();
// value() — validate a constant or already-resolved object, not a binding
String currentStatus = resolveStatus();
Rule statusCheck = notNull(value(currentStatus))
.errorCode("STATUS-001")
.message("Status must be set.")
.build();
// size — String / Collection / Array length within [min, max]
Rule nameLength = size(binding("name"), 2, 50)
.errorCode("USER-003")
.message("Name must be between 2 and 50 characters.")
.build();
// min / max — numeric bounds
Rule ageMin = min(binding("age"), 18)
.errorCode("USER-004")
.message("Age must be at least 18.")
.build();
Rule quantityMax = max(binding("quantity"), 100)
.errorCode("ORDER-001")
.build();
// decimalMin / decimalMax — decimal / BigDecimal bounds
Rule priceMin = decimalMin(binding("price"), 0.01)
.errorCode("PRICE-001")
.message("Price must be greater than zero.")
.build();
Rule discountMax = decimalMax(binding("discount"), 0.99)
.errorCode("PRICE-002")
.build();
// digits — at most N integer digits and M fractional digits
Rule amount = digits(binding("amount"), 8, 2)
.errorCode("AMOUNT-001")
.message("Amount must have at most 8 integer digits and 2 decimal places.")
.build();
// pattern — match a regular expression
Rule postalCode = pattern(binding("postalCode"), "\\d{5}(-\\d{4})?")
.errorCode("ADDR-001")
.message("Postal code must be in ZIP or ZIP+4 format.")
.build();
// positive / positiveOrZero — sign checks
Rule positiveScore = positive(binding("score"))
.errorCode("SCORE-001")
.build();
// notEmpty with value() — check a list resolved outside the context
List<String> roles = loadRoles(userId);
Rule hasRoles = notEmpty(value(roles))
.errorCode("AUTH-001")
.message("User must have at least one role assigned.")
.build();
Validation Rules in a RuleSet
Predefined validation rules are plain Rule instances and compose into a RuleSet the same way any other rule does. Every rule that fires a violation automatically writes it to the ruleViolations binding, so all violations are collected in a single pass.
User registration
import static org.rulii.validation.rules.Validators.*;
RuleSet<?> registrationRules = RuleSet.builder()
.with("UserRegistration")
.rule(notBlank(binding("username"))
.errorCode("REG-001").message("Username is required.").build())
.rule(size(binding("username"), 3, 20)
.errorCode("REG-002").message("Username must be 3–20 characters.").build())
.rule(alphaNumeric(binding("username"))
.errorCode("REG-003").message("Username may only contain letters and digits.").build())
.rule(notBlank(binding("email"))
.errorCode("REG-004").message("Email is required.").build())
.rule(email(binding("email"))
.errorCode("REG-005").message("Email address is not valid.").build())
.rule(min(binding("age"), 18)
.errorCode("REG-006").message("You must be at least 18 years old.").build())
.build();
RuleViolations violations = new RuleViolations();
registrationRules.run(
username -> "jo",
email -> "not-an-email",
age -> 16,
ruleViolations -> violations
);
// REG-002, REG-005, REG-006 will be present
violations.hasErrors(); // true
Order line validation
import static org.rulii.validation.rules.Validators.*;
RuleSet<?> orderLineRules = RuleSet.builder()
.with("OrderLineValidation")
.rule(notBlank(binding("productCode"))
.errorCode("ORD-001").message("Product code is required.").build())
.rule(min(binding("quantity"), 1)
.errorCode("ORD-002").message("Quantity must be at least 1.").build())
.rule(max(binding("quantity"), 999)
.errorCode("ORD-003").message("Quantity cannot exceed 999.").build())
.rule(decimalMin(binding("unitPrice"), 0.01)
.errorCode("ORD-004").message("Unit price must be greater than zero.").build())
.rule(digits(binding("unitPrice"), 8, 2)
.errorCode("ORD-005").message("Unit price must have at most 8 integer and 2 decimal digits.").build())
.build();
RuleViolations violations = new RuleViolations();
orderLineRules.run(
productCode -> "SKU-42",
quantity -> 0,
unitPrice -> 12.5,
ruleViolations -> violations
);
// ORD-002 fires — quantity 0 is below the minimum of 1
violations.hasErrors(); // true
Iterating violations
// Check overall result
if (violations.hasErrors()) {
for (RuleViolation v : violations) {
System.out.printf("[%s] %s%n", v.getErrorCode(), v.getErrorMessage());
}
}
// Or collect error codes only
List<String> codes = violations.stream()
.map(RuleViolation::getErrorCode)
.collect(Collectors.toList());
Direct Instantiation is Not Supported
As of 1.2.0, predefined validation rule classes (NotNullValidationRule, EmailValidationRule, etc.) are no longer intended to be instantiated directly. Their constructors are not part of the public API and may change between releases.
Always create built-in rules through the Validators static factory. This keeps your code decoupled from the underlying implementation classes and ensures you get the correct builder configuration every time.
import static org.rulii.validation.rules.Validators.*;
// Correct — use the factory
Rule r = notNull(binding("email")).build();
// Incorrect — do not instantiate directly
// Rule r = new NotNullValidationRule(...);
Available Validators
| Factory Method | What It Checks |
|---|---|
| notNull(fn) | Value is not null |
| isNull(fn) | Value is null |
| notEmpty(fn) | Collection / String / Array is not empty |
| notBlank(fn) | String is not blank (not null, not empty, not whitespace) |
| blank(fn) | Value is blank (null or whitespace-only) |
| size(fn, min, max) | String / Collection / Array size is within [min, max] |
| min(fn, min) | Numeric value ≥ min |
| max(fn, max) | Numeric value ≤ max |
| decimalMin(fn, min) | Decimal value ≥ min |
| decimalMax(fn, max) | Decimal value ≤ max |
| positive(fn) | Number > 0 |
| positiveOrZero(fn) | Number ≥ 0 |
| negative(fn) | Number < 0 |
| negativeOrZero(fn) | Number ≤ 0 |
| digits(fn, maxInt, maxFrac) | At most maxInt integer digits and maxFrac fractional digits |
| email(fn) | Valid e-mail address format |
| url(fn) | Valid URL format |
| pattern(fn, regex) | Matches the given regular expression |
| assertTrue(fn) | Value is true |
| assertFalse(fn) | Value is false |
| assertEquals(fn, expected) | Value equals the expected value |
| assertNotEquals(fn, unexpected) | Value does not equal the given value |
| future(fn) | Date / time value is in the future |
| futureOrPresent(fn) | Date / time value is now or in the future |
| past(fn) | Date / time value is in the past |
| pastOrPresent(fn) | Date / time value is now or in the past |
| alpha(fn) | String contains only alphabetic characters |
| alphaNumeric(fn) | String contains only alphanumeric characters |
| ascii(fn) | String contains only ASCII characters |
| numeric(fn) | String contains only digit characters |
| decimal(fn) | String is a valid decimal number |
| lowerCase(fn) | String is all lower-case |
| upperCase(fn) | String is all upper-case |
| startsWith(fn, prefixes...) | String starts with one of the given prefixes |
| endsWith(fn, suffixes...) | String ends with one of the given suffixes |
| in(fn, collection) | Value is contained in the given collection |
| fileExists(fn) | Value is a path to an existing file |