Zod Schema / conformZodMessage

conformZodMessage

A set of custom messages to control the validation behavior. This is useful if you need async validation for one of the fields.

#Options

conformZodMessage.VALIDATION_SKIPPED

This message is used to indicate that the validation is skipped and Conform should use the previous result instead.

conformZodMessage.VALIDATION_UNDEFINED

This message is used to indicate that the validation is not defined and Conform should fallback to server validation.

#Example

Here is a signup form example which validates if the email is unique.

1import type { Intent } from '@conform-to/react';
2import { useForm } from '@conform-to/react';
3import { parseWithZod, conformZodMessage } from '@conform-to/zod';
4// If you are using Zod v4, update the imports:
5// import { parseWithZod, conformZodMessage } from '@conform-to/zod/v4';
6import { z } from 'zod';
7
8// Instead of sharing a schema, prepare a schema creator
9function createSchema(
10  // The `intent` will be provided by the `parseWithZod` helper
11  intent: Intent | null,
12  options?: {
13    isEmailUnique: (email: string) => Promise<boolean>;
14  },
15) {
16  return z
17    .object({
18      email: z
19        .string()
20        .email()
21        // Pipe the schema so it runs only if the email is valid
22        .pipe(
23          z.string().superRefine((email, ctx) => {
24            const isValidatingEmail =
25              intent === null ||
26              (intent.type === 'validate' && intent.payload.name === 'email');
27
28            if (!isValidatingEmail) {
29              ctx.addIssue({
30                code: 'custom',
31                message: conformZodMessage.VALIDATION_SKIPPED,
32              });
33              return;
34            }
35
36            if (typeof options?.isEmailUnique !== 'function') {
37              ctx.addIssue({
38                code: 'custom',
39                message: conformZodMessage.VALIDATION_UNDEFINED,
40                fatal: true,
41              });
42              return;
43            }
44
45            return options.isEmailUnique(email).then((isUnique) => {
46              if (!isUnique) {
47                ctx.addIssue({
48                  code: 'custom',
49                  message: 'Email is already used',
50                });
51              }
52            });
53          }),
54        ),
55    })
56    .and(
57      z
58        .object({
59          password: z.string({ required_error: 'Password is required' }),
60          confirmPassword: z.string({
61            required_error: 'Confirm password is required',
62          }),
63        })
64        .refine((data) => data.password === data.confirmPassword, {
65          message: 'Password does not match',
66          path: ['confirmPassword'],
67        }),
68    );
69}
70
71export async function action({ request }) {
72  const formData = await request.formData();
73  const submission = await parseWithZod(formData, {
74    schema: (intent) =>
75      // create the zod schema based on the intent
76      createSchema(intent, {
77        isEmailUnique(email) {
78          // query from your database
79        },
80      }),
81    async: true,
82  });
83
84  if (submission.status !== 'success') {
85    return submission.reply();
86  }
87
88  // ...
89}
90
91export default function Signup() {
92  const lastResult = useActionData();
93  const [form, fields] = useForm({
94    lastResult,
95    onValidate({ formData }) {
96      return parseWithZod(formData, {
97        // Create the schema without `isEmailUnique` defined
98        schema: (intent) => createSchema(intent),
99      });
100    },
101  });
102
103  // ...
104}