Custom Validators
Custom validators extend the built-in rule set with your own business logic. Register a named validator once using registerValidator(), then reference it by name in any form schema. Custom validators can be synchronous or asynchronous and receive the current value, field ID, and all form values as arguments.
Validator Function Signature
// packages/validation-js/src/customValidators.ts
type ValidatorFn = (
value: any, // Current field value
fieldId: string, // The field's id from schema
allValues: Record<string, any> // All current form values
) => ValidationResult | Promise<ValidationResult>;
interface ValidationResult {
valid: boolean;
message?: string; // Only shown if valid === false
}
Registering a Custom Validator
import { registerValidator } from '@atlas-forms/validation-js';
// Synchronous validator — Luhn algorithm for credit card numbers
registerValidator('luhnCheck', (value) => {
const digits = String(value).replace(/\D/g, '');
let sum = 0;
let alternate = false;
for (let i = digits.length - 1; i >= 0; i--) {
let n = parseInt(digits[i], 10);
if (alternate) {
n *= 2;
if (n > 9) n -= 9;
}
sum += n;
alternate = !alternate;
}
return {
valid: sum % 10 === 0,
message: 'Invalid card number'
};
});
// Async validator — check username availability
registerValidator('usernameAvailable', async (value) => {
if (!value || value.length < 3) {
return { valid: true }; // Let minLength handle short values
}
const res = await fetch(`/api/users/check-username?username=${encodeURIComponent(value)}`);
const { available } = await res.json();
return {
valid: available,
message: `"${value}" is already taken — please choose a different username`
};
});
Using a Registered Validator in Schema
// Reference by name in the form schema
{
"id": "card-number",
"type": "text",
"label": "Card Number",
"settings": { "placeholder": "XXXX XXXX XXXX XXXX" },
"validation": {
"required": true,
"pattern": "^[0-9 ]{13,19}$",
"customRule": {
"name": "luhnCheck",
"message": "Card number is invalid"
}
}
}
Inline Expression Validator
For simple inline rules that don't need a registered function, use the expression property in customRule. The expression must evaluate to true when the value is valid:
{
"id": "quantity",
"type": "number",
"label": "Quantity",
"validation": {
"required": true,
"min": 1,
"customRule": {
"expression": "value % 1 === 0",
"message": "Quantity must be a whole number"
}
}
}
// Another inline example — value must be divisible by 5
{
"id": "lot-size",
"type": "number",
"label": "Lot Size",
"validation": {
"customRule": {
"expression": "value % 5 === 0",
"message": "Lot size must be a multiple of 5"
}
}
}
Where to Register Validators
Register all custom validators once at application startup, before any form is rendered. In a React application this is typically in the app entry point or a dedicated validation setup module:
// src/validators/index.ts — all custom validators in one file
import { registerValidator } from '@atlas-forms/validation-js';
import { luhnCheck } from './luhnCheck';
import { usernameAvailable } from './usernameAvailable';
import { ibanChecksum } from './ibanChecksum';
import { postcodeExists } from './postcodeExists';
export function registerAllValidators(): void {
registerValidator('luhnCheck', luhnCheck);
registerValidator('usernameAvailable', usernameAvailable);
registerValidator('ibanChecksum', ibanChecksum);
registerValidator('postcodeExists', postcodeExists);
}
// src/main.tsx
import { registerAllValidators } from './validators';
registerAllValidators(); // Before ReactDOM.render(...)
Validator Execution Order
When multiple custom rules are applied to a field, they execute in sequence. If a synchronous rule fails, asynchronous rules are skipped to avoid unnecessary network calls:
- All built-in rules (synchronous)
- Synchronous custom validators (in
customRulesarray order) - Asynchronous custom validators (only if all sync rules passed)