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 />;
}