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