Skip to content

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 ​

bash
npm install zod
tsx
import { 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 ​

bash
npm install yup
tsx
import * 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 (which handleSubmit and setValue(..., true) call internally) against the whole current values object.
  • Its errors (keyed by field id / object path) are merged with any per-field validations already 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>:

ts
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:

ts
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.