Skip to content

Validation

Validation

Pick where the rule belongs: field config, top-level schema, field.validation, asyncValidation, crossFieldValidation, or inlineValidation. Each section links to source or tests.

Validation overview

Field validators run on change, blur, and submit. Each pass checks built-ins, field.validation, schema, then cross-field rules; async checks run on change.

  • The hook calls buildFormValidators(config.schema, config.crossFieldValidation) and buildFieldValidators(field, config.schema, config.crossFieldValidation, config.asyncValidation).
  • A returned string becomes the field message. null or undefined passes. false falls back to Invalid value, except cross-field rules fall back to Invalid field combination.
  • The pipeline tests build validators directly, so these examples follow the tested runtime.

Built-in constraints

Put simple field-local checks on the field config. required handles empty values first; email, maxLength, min, and max run after a value exists.

  • required returns “{label} is required” when the value is empty.
  • type: email returns “Please enter a valid email address” when the value is a non-empty invalid email string.
  • maxLength checks strings. min and max check numbers. The job example uses min: 0 for salaryExpectation.

Form-level schema

Put the Standard Schema object on the top-level schema option. Keep formOptions for defaultValues and submit handlers.

  • buildFormValidators maps schema issues into TanStack Form field errors on change, blur, and submit.
  • buildFieldValidators also asks the same schema for the current field message, so a field can show its schema error during normal field validation.
  • The survey, job application, and typed-hook-usage examples all place schema beside fields.

Field-level validation

Put one-field sync rules on field.validation. Return a string, null, undefined, or false.

  • Function validation receives value, values, and a context object containing value, values, and fieldName.
  • A direct field schema is also supported, for example z.string().min(3, “Username must be at least 3 characters”).
  • Return a specific string when the UI should show specific copy. Return false only when the fallback or object message is good enough.

Async validation

Put server-backed or delayed checks in asyncValidation, keyed by field name. The validator receives value, values, and AbortSignal.

  • Return a string for a server message, null or undefined to pass, or false to show Invalid value.
  • Use signal in fetch so newer keystrokes can cancel older requests cleanly.
  • loadingMessage is currently typed config metadata. The validator builder reads validator and debounceMs; no built-in renderer or runtime currently consumes loadingMessage.

Cross-field validation

Put multi-field rules in crossFieldValidation. List every field the rule touches so sibling changes trigger validation.

  • The runtime derives onChangeListenTo from the fields list, so confirmPassword revalidates when password changes.
  • Return a string for the clearest field error. Returning false maps to Invalid field combination.
  • Keep these rules top-level so buildFormValidators and buildFieldValidators can both see them.

Inline validation

Put field-owned async on-change rules in inlineValidation. It uses the same return contract as asyncValidation.

  • enabled must be true before the runtime builds the inline async validator.
  • When asyncValidation for the same field exists, that rule runs first and its debounceMs wins.
  • inlineValidation.validator receives value, current values, and AbortSignal. Return false for the Invalid value fallback.

Typed fields over TanStack Form

Formedible renders shadcn-compatible fields while keeping TanStack Form in reach.

import { z } from 'zod';

import { useFormedible } from '@/components/ui/formedible/hooks/use-formedible';

const onboardingSchema = z.object({
  name: z.string().min(2),
  plan: z.enum(['starter', 'team', 'enterprise']),
  needsMigration: z.boolean(),
});

type OnboardingValues = z.infer<typeof onboardingSchema>;

type WorkspaceRecord = OnboardingValues & {
  slug: string;
};

const workspaceRecords: WorkspaceRecord[] = [];

function createWorkspace(values: OnboardingValues): WorkspaceRecord {
  const workspace = {
    ...values,
    slug: values.name.trim().toLowerCase().replace(/\s+/g, '-'),
  };

  workspaceRecords.push(workspace);
  return workspace;
}

export function OnboardingForm() {
  const { Form } = useFormedible<OnboardingValues>({
    fields: [
      { name: 'name', type: 'text', label: 'Workspace name', required: true },
      { name: 'plan', type: 'radio', label: 'Plan', options: ['starter', 'team', 'enterprise'] },
      { name: 'needsMigration', type: 'switch', label: 'Import an existing form system?' },
    ],
    schema: onboardingSchema,
    formOptions: {
      defaultValues: { name: '', plan: 'team', needsMigration: false },
      onSubmit: ({ value }) => {
        createWorkspace(value);
      },
    },
  });

  return <Form />;
}

Formedible

TanStack Form, shadcn/ui, Zod