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 _form if truly cross-cutting, or the error(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 (whose CommandContext exposes a DataAccessor for data access).

Related