ValidatorExtension
ValidatorExtension adds programmatic validation rules on top of the declarative DSL constraints in models.json (required, min, max, regex, enum). Use a validator when the rule depends on another record (uniqueness, foreign-key existence), on the current user (org-scope checks), or on external state.
Concept
A validator is a field-level validation extension, registered and resolved by the namespace:validator-name key returned from getValidatorKey(). validate(ValidationContext) receives the value under validation (context.value()), its field code (context.fieldCode()), and the other fields of the same record (context.recordData()), and returns a ValidationResult — either success() or an aggregated set of field errors. Returning errors surfaces field-level error toasts in the UI (the platform maps field → form-control error, never a generic "Bad parameter" toast — see red line §2.2).
DSL constraints and ValidatorExtensions are additive; both must pass.
Interface signature
package com.auraboot.framework.plugin.extension;
import org.pf4j.ExtensionPoint;
public interface ValidatorExtension extends ExtensionPoint {
String getValidatorKey();
ValidationResult validate(ValidationContext context);
// default methods: supports(validatorKey) / getOrder() / isFailFast()
}ValidationResult API (ValidationResult and ValidationContext are both nested records on ValidatorExtension):
ValidationResult.success();
ValidationResult.error("A lead with this email already exists");
ValidationResult.error("email", "A lead with this email already exists");
ValidationResult.errors(List.of(
new ValidationError("email", "Already exists"),
new ValidationError("phone", "Invalid format")
));The field argument of error(field, message) must be a valid model field code; the platform maps it to the form control. For cross-field errors, use a synthetic field code such as _form, or use error(message) to omit the field.
Implementation example
Validate a lead's email field format and dependencies (field-level, pure read):
package com.acme.crm.validator;
import com.auraboot.framework.plugin.extension.ValidatorExtension;
import org.pf4j.Extension;
import java.util.Map;
@Extension
public class LeadEmailValidator implements ValidatorExtension {
@Override
public String getValidatorKey() { return "crm:lead-email"; }
@Override
public ValidationResult validate(ValidationContext ctx) {
Object value = ctx.value();
if (value == null || ((String) value).isBlank()) {
return ValidationResult.success(); // DSL `required` already handles the missing case
}
String email = (String) value;
if (!email.contains("@")) {
return ValidationResult.error(ctx.fieldCode(),
"$i18n:crm.lead.email.invalid");
}
// Other fields of the same record are available from recordData for cross-field checks
Map<String, Object> record = ctx.recordData();
// ...
return ValidationResult.success();
}
}Note the $i18n: prefix — surfacing raw English in errors violates red line §3.
The ValidationContext record also exposes tenantId(), namespace(), validatorKey(), validatorParams(), and settings() metadata accessors.
Registration
@Extension only. Multiple validators sharing the same validatorKey run in ascending getOrder(); isFailFast() defaults to false, so all errors are collected and returned in a single response — users see every problem at once.
Common pitfalls
- Returning a generic error without a
field. Surfaces as a banner toast, not a form-control error — violates red line §2.2. Use_formif truly cross-cutting, or theerror(message)overload. - Doing expensive validation unconditionally. Short-circuit on cheap checks first (e.g. return
success()immediately when the value is empty). - Mixing validation with mutation. Validators must be pure reads. Mutation belongs in
CommandHandlerExtension(whoseCommandContextexposes aDataAccessorfor data access).
Related
- CommandHandlerExtension —
CommandHandlerExtension.executefor mutation,ValidatorExtension.validatefor pure checks - Models DSL — declarative DSL constraints
- Commands pipeline — the command pipeline