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