Portal Community

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:

  1. All built-in rules (synchronous)
  2. Synchronous custom validators (in customRules array order)
  3. Asynchronous custom validators (only if all sync rules passed)
Debouncing Async Validators The validation engine automatically debounces async validators by 400ms to avoid flooding the server with requests on every keystroke. You do not need to implement debouncing inside your validator function.