Validation Guide
Validation in react-form-dto is declarative. Each field in your FormDTO carries a validations object — the library runs those rules automatically on submit, on touch, or on demand.
For cross-field rules, async checks, or reusing a schema you already maintain elsewhere, see the Schema Validation (Zod / Yup) guide instead — the two approaches can be combined.
Built-in rules
| Rule | Type | Description |
|---|---|---|
required | boolean | string | Record<string, string> | Field must not be empty — an empty string, null/undefined, or an empty array (e.g. an untouched multi-select) all count as empty. Pass true for a default message, a string for a custom one, or a locale map ({ en: "...", fr: "..." }) for i18n. |
minLength | number | Minimum string length. |
maxLength | number | Maximum string length. |
min | number | Minimum numeric value. |
max | number | Maximum numeric value. |
pattern | RegExp | Value must match the regex. |
validate | (value, allValues) => string | null | Custom function, called with the field's value and the full current form values (for cross-field checks) — return a string for an error, null for valid. |
min/max apply to number fields; minLength/maxLength apply to string fields. There is currently no built-in options-membership or dateRange rule — use validate for those.
Defining rules on a field
import type { FormDTO } from 'react-form-dto';
const myForm: FormDTO = {
sections: [
{
id: "account",
fields: [
{
id: "email",
type: "email",
label: "Email",
validations: {
required: "Email is required",
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
minLength: 5,
maxLength: 100,
},
},
{
id: "age",
type: "number",
label: "Age",
validations: {
required: true,
min: 18,
max: 99,
},
},
{
id: "username",
type: "text",
label: "Username",
validations: {
required: "Username is required",
minLength: 3,
maxLength: 20,
validate: (value) =>
/^[a-z0-9_]+$/.test(value)
? null
: "Only lowercase letters, numbers, and underscores",
},
},
],
},
],
};Validating on submit
The recommended approach is useFormDTO + FormProvider. form.handleSubmit validates all fields before calling your callback — if any field is invalid the callback is not called and errors are shown inline.
import { FormBuilder, FormProvider, useFormDTO } from 'react-form-dto';
import { myForm } from './myFormDTO';
export function AccountForm() {
const form = useFormDTO(myForm);
const onSubmit = (data: Record<string, any>) => {
// Only reached when all fields pass validation
console.log('Valid data:', data);
};
return (
<FormProvider value={form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormBuilder dto={myForm} />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
}Validating programmatically
Use form.trigger() when you need to validate without submitting — for example, a "Next" button in a multi-step form.
Breaking change in 2.0
form.trigger() now returns Promise<boolean> instead of boolean — it needs to be async so a schema resolver (which may itself call an API) can be awaited. Add await at every call site.
import { FormBuilder, FormProvider, useFormDTO } from 'react-form-dto';
import { stepOneForm } from './stepOneForm';
export function StepOne({ onNext }: { onNext: () => void }) {
const form = useFormDTO(stepOneForm);
const handleNext = async () => {
const isValid = await form.trigger(); // validate all fields
if (isValid) onNext();
};
// Validate a single field
const checkEmail = async () => {
const emailOk = await form.trigger('email');
console.log('Email valid:', emailOk);
};
return (
<FormProvider value={form}>
<FormBuilder dto={stepOneForm} />
<button type="button" onClick={handleNext}>Next</button>
</FormProvider>
);
}Custom validation
The validate function receives the field's current value. It runs inside useFormDTO's validation engine, which passes the full current form snapshot via valuesRef — enabling cross-field comparisons:
{
id: "confirmPassword",
type: "password",
label: "Confirm Password",
validations: {
required: "Please confirm your password",
validate: (value, allValues) =>
value !== allValues?.password ? "Passwords do not match" : null,
},
}Injecting server-side errors
After a failed API call, set errors manually using form.setError:
const onSubmit = async (data: Record<string, any>) => {
try {
await api.register(data);
} catch (err) {
form.setError('email', 'This email is already registered');
}
};Clear them with form.clearError('email') when the user edits the field, or call form.reset() to wipe all errors.
Error messages and i18n
All built-in rule messages can be overridden by passing a string instead of true:
validations: {
required: "Dieses Feld ist erforderlich",
minLength: 3, // uses the default message
}For fully localised apps, pass locale to useFormDTO:
const form = useFormDTO(myForm, { locale: 'de' });Field labels and error strings defined as locale maps in the DTO are resolved to the given locale automatically.
Notes
- Errors are shown per-field inline below each input.
- With
useFormDTO+FormProvider, errors only appear after a field is first touched (or after submit is attempted) — not on initial render. - The
validatecustom function should returnnull(notundefinedorfalse) to indicate the field is valid. - Nested fields inside a
"group"field are validated the same way top-level fields are. Items inside an"array"field are not individually validated yet — validate array contents with a top-levelvalidatefunction, or a schema resolver, in the meantime.