form-schema-runtime

Built-in validators, custom synchronous validators, errors, and summaries.

Documentation pages
On this page
  1. Validation Lifecycle
  2. Built-In Validation
  3. required
  4. minLength
  5. maxLength
  6. min
  7. max
  8. pattern
  9. email
  10. Custom Built-In Messages
  11. Custom Synchronous Validators
  12. Simple Custom Validator
  13. Validator Using Other Values
  14. Field-Level Errors
  15. Form-Level Error Summary
  16. Conditional Fields
  17. onValidationError
  18. validate API
  19. What Is Out of Scope

Validation Guide

Validation in form-schema-runtime is synchronous, deterministic, and independent from DOM rendering. The runtime validates current visible fields, stores errors in state, and renders field-level errors plus a form-level error summary.

Validation Lifecycle

Validation runs when:

  • the user submits the rendered form
  • application code calls form.validate()
  • a value changes while the form already has errors

When validation fails:

  • invalid controls receive aria-invalid="true"
  • field-level error text is rendered
  • the error text is referenced by aria-describedby
  • a form-level error summary is rendered
  • error summary links focus invalid controls
  • onValidationError runs when validation was triggered by submit or validate()

Hidden conditional fields are skipped, so stale errors from hidden fields do not block submission.

Built-In Validation

required

{
  type: "text",
  name: "fullName",
  label: "Full name",
  required: true
}

Required validation checks for meaningful values. Empty strings, null, and undefined are invalid. Required checkboxes must be checked.

minLength

{
  type: "password",
  name: "password",
  label: "Password",
  minLength: 12
}

minLength checks string length when a value is present.

maxLength

{
  type: "textarea",
  name: "message",
  label: "Message",
  maxLength: 500
}

maxLength checks string length when a value is present.

min

{
  type: "number",
  name: "amount",
  label: "Amount",
  min: 1
}

min checks numeric values.

max

{
  type: "number",
  name: "amount",
  label: "Amount",
  max: 250000
}

max checks numeric values.

pattern

{
  type: "text",
  name: "employeeId",
  label: "Employee ID",
  pattern: "^[A-Z]{2}-[0-9]{5}$",
  validationMessages: {
    pattern: "Employee ID must look like IT-12345."
  }
}

pattern is a JavaScript regular expression source string. It is used only for validation; schemas do not execute JavaScript.

email

{
  type: "email",
  name: "email",
  label: "Email",
  required: true
}

Email validation runs automatically for email fields.

Custom Built-In Messages

Use validationMessages to replace a built-in message for one field:

{
  type: "email",
  name: "email",
  label: "Work email",
  required: true,
  validationMessages: {
    required: "Enter a work email address.",
    email: "Use a valid work email address."
  }
}

Supported keys are required, minLength, maxLength, min, max, pattern, email, and number. The number key controls the fallback used when numeric range validation receives a non-numeric value. Custom validator messages come directly from the registered validator and are not configured through validationMessages.

Custom Synchronous Validators

Schemas reference validators by name:

const schema: FormSchema = {
  id: "customer-form",
  fields: [
    {
      type: "text",
      name: "taxCode",
      label: "Tax code",
      validators: ["taxCode"]
    }
  ]
};

Application code registers validator functions:

const form = createForm({
  container,
  schema,
  validators: {
    taxCode(value) {
      if (!value) {
        return null;
      }

      return String(value).length === 16 ? null : "Tax code must contain 16 characters.";
    }
  }
});

A custom validator returns null when valid and a string message when invalid.

Simple Custom Validator

const validators: CustomValidatorMap = {
  noAdmin(value) {
    return String(value).toLowerCase() === "admin" ? "This value is reserved." : null;
  }
};

Validator Using Other Values

const validators: CustomValidatorMap = {
  amountRequiresPurchaseOrder(value, context) {
    const amount = Number(context.values.amount ?? 0);
    const purchaseOrder = String(value ?? "");

    if (amount > 10000 && purchaseOrder.length < 6) {
      return "A purchase order is required for amounts above 10000.";
    }

    return null;
  }
};

Validator context includes:

  • fieldName
  • values
  • schema

Field-Level Errors

Errors are stored as:

type FieldErrors = Record<string, string[]>;

Example:

{
  email: ["Email must be a valid email address."]
}

The first error for a field is rendered next to that field and in the form-level summary.

Form-Level Error Summary

When validation fails, the runtime renders a summary with links to invalid fields. Clicking a summary link focuses the matching control.

This behavior helps keyboard and screen reader users find errors without searching through the full form.

Conditional Fields

Conditional fields are validated only when visible:

{
  type: "textarea",
  name: "adminJustification",
  label: "Administrator access justification",
  required: true,
  visibleWhen: {
    field: "accessLevel",
    equals: "admin"
  }
}

If accessLevel is not admin, adminJustification does not block submission.

onValidationError

const form = createForm({
  container,
  schema,
  onValidationError(errors, state) {
    console.warn(errors);
    console.log(state.visibleFields);
  }
});

onValidationError is useful for analytics, status text, logging, or synchronizing validation state with surrounding UI.

validate API

const isValid = form.validate();

if (isValid) {
  submitValues(form.getValues());
}

validate() updates rendered errors and returns a boolean.

What Is Out of Scope

Async validation is intentionally out of scope in v1. It requires loading state, cancellation, race handling, and backend policy decisions.

Schemas do not execute JavaScript. Custom validators are application code registered through createForm; schema data only references validator names.