Create custom validation logic with lambda expressions and method references
While Validator provides 27 predefined rules, real-world applications often require custom validation logic. The .rule() method allows you to define arbitrary validation logic using lambda expressions or method references.
import io.github.ApamateSoft.validator.Validator;Validator validator = new Validator.Builder() .rule("Enter a text other than null", (String evaluate) -> { return evaluate != null; }) .rule("The text must equal 'xxx'", (String evaluate) -> { return evaluate.equals("xxx"); }) .build();
Custom rules are evaluated in the same sequential order as predefined rules. When a custom rule fails, the error message is returned and remaining rules are skipped.
// Check if not nullvalidator.rule("Value cannot be null", value -> value != null);// Check specific valuevalidator.rule("Must be 'admin'", value -> value.equals("admin"));// Check lengthvalidator.rule("Must be more than 10 characters", value -> value.length() > 10);// Check starts withvalidator.rule("Must start with 'USER_'", value -> value.startsWith("USER_"));// Check ends withvalidator.rule("Must end with '.com'", value -> value.endsWith(".com"));// Check containsvalidator.rule("Must contain '@'", value -> value.contains("@"));
Lambdas can access external variables and services:
public class SubscriptionValidator { private final PlanService planService; private final UserService userService; private final String currentUserId; public SubscriptionValidator(PlanService planService, UserService userService, String currentUserId) { this.planService = planService; this.userService = userService; this.currentUserId = currentUserId; } public Validator createPlanValidator() { return new Validator.Builder() .required("Plan ID is required") .rule("Plan does not exist", planId -> { return planService.exists(planId); }) .rule("Plan is not available", planId -> { Plan plan = planService.findById(planId); return plan != null && plan.isActive(); }) .rule("User already subscribed to this plan", planId -> { return !userService.hasActivePlan(currentUserId, planId); }) .rule("User doesn't meet plan requirements", planId -> { User user = userService.findById(currentUserId); Plan plan = planService.findById(planId); return plan != null && plan.meetsRequirements(user); }) .build(); }}
public class ConditionalValidator { private final String accountType; public ConditionalValidator(String accountType) { this.accountType = accountType; } public Validator createAccountNumberValidator() { Validator.Builder builder = new Validator.Builder() .required("Account number is required"); if ("business".equals(accountType)) { builder.rule("Business accounts must start with 'B'", num -> num.startsWith("B")); builder.minLength(10, "Business account must be 10 digits"); } else { builder.rule("Personal accounts must start with 'P'", num -> num.startsWith("P")); builder.minLength(8, "Personal account must be 8 digits"); } return builder.build(); }}
validator.rule("Invalid format", value -> { try { // Safely parse and validate int number = Integer.parseInt(value); return number > 0; } catch (NumberFormatException e) { // Invalid number format return false; }});
validator.rule("Must contain hyphen", value -> { // Always check for null if not using @Required first if (value == null) { return false; } return value.contains("-");});// Better: Use required() firstValidator validator = new Validator.Builder() .required() // Ensures non-null .rule("Must contain hyphen", value -> value.contains("-")) .build();
Always consider null and edge cases in custom rules, or ensure they come after required() in the validation chain.
Each custom rule should validate one specific condition. This makes error messages clear and rules reusable.
// Good: Separate rules.rule("Username taken", this::isUsernameAvailable).rule("Username contains profanity", this::isProfanityFree)// Bad: Multiple checks in one rule.rule("Username invalid", username -> isUsernameAvailable(username) && isProfanityFree(username))
Provide clear, actionable error messages
Error messages should tell users exactly what’s wrong and how to fix it.
// Good.rule("Email domain not allowed. Use your company email.", email -> email.endsWith("@company.com"))// Bad.rule("Invalid", email -> email.endsWith("@company.com"))
Consider performance for expensive operations
If custom rules make database calls or API requests, consider caching or batching.
// Cache results for repeated validationprivate final Map<String, Boolean> usernameCache = new HashMap<>();private boolean isUsernameAvailable(String username) { return usernameCache.computeIfAbsent(username, key -> !userRepository.existsByUsername(key) );}
Order rules by cost
Put cheap validations first, expensive ones last.
new Validator.Builder() .required() // Fast .minLength(3) // Fast .onlyAlphanumeric() // Fast .rule("Username taken", this::checkDb) // Slow (database) .build();
Custom rules give you unlimited flexibility to implement any validation logic your application requires, from simple format checks to complex business rules involving multiple systems.