Schema Validation (Zod / Yup)
react-form-dto's built-in validations rules cover the common cases (required, min/max, pattern, etc.), but for cross-field rules, refinements, or a schema you already maintain elsewhere, pass a resolver to useFormDTO (or the lower-level useForm) instead.
This mirrors the resolver option in React Hook Form, so the mental model carries over directly.
Zod
npm install zodimport { z } from 'zod';
import { FormBuilder, FormProvider, useFormDTO, zodResolver } from 'react-form-dto';
import type { FormDTO } from 'react-form-dto';
const schema = z
.object({
email: z.string().email('Enter a valid email'),
password: z.string().min(8, 'At least 8 characters'),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
const signupForm: FormDTO = {
sections: [
{
id: 'account',
fields: [
{ id: 'email', type: 'email', label: 'Email' },
{ id: 'password', type: 'password', label: 'Password' },
{ id: 'confirmPassword', type: 'password', label: 'Confirm Password' },
],
},
],
};
export function SignupForm() {
const form = useFormDTO(signupForm, { resolver: zodResolver(schema) });
return (
<FormProvider value={form}>
<form onSubmit={form.handleSubmit((data) => console.log(data))}>
<FormBuilder dto={signupForm} />
<button type="submit">Sign up</button>
</form>
</FormProvider>
);
}Yup
npm install yupimport * as yup from 'yup';
import { useFormDTO, yupResolver } from 'react-form-dto';
const schema = yup.object({
email: yup.string().email('Enter a valid email').required('Required'),
age: yup.number().min(18, 'Must be at least 18').required('Required'),
});
const form = useFormDTO(myDTO, { resolver: yupResolver(schema) });How resolver errors are merged
- The resolver runs on every
trigger()call (whichhandleSubmitandsetValue(..., true)call internally) against the whole current values object. - Its
errors(keyed by field id / object path) are merged with any per-fieldvalidationsalready declared on the DTO. - If both a DTO-level rule and the resolver report an error for the same field, the resolver's message wins.
This means you can keep simple required/pattern rules directly on the DTO for the fields that need them, and reserve the resolver for validation the DTO's declarative rules can't express (cross-field checks, refinements, existing shared schemas).
form.trigger() is now async
Because a resolver may run asynchronously (e.g. one backed by an API call), trigger() returns Promise<boolean>:
const isValid = await form.trigger();
const emailOk = await form.trigger('email');form.handleSubmit(onSubmit) already awaits validation internally — no change needed there.
Writing your own resolver
A resolver is just a function matching the Resolver<T> type:
import type { Resolver } from 'react-form-dto';
const myResolver: Resolver<{ email: string }> = async (values) => {
const errors: Record<string, string> = {};
if (!values.email.includes('@')) {
errors.email = 'Enter a valid email';
}
return { values, errors };
};zodResolver and yupResolver are thin adapters around this same shape — use them as a reference if you need to integrate a different validation library.