How to Implement Form Validation Using Zod with the Field Component in refine-shadcn

Use zodResolver from @hookform/resolvers/zod to connect your Zod schema to useForm from @refinedev/react-hook-form, then spread the returned form object onto the Field component to automatically validate inputs and display error messages.

The refine-shadcn library (available at ferdiunal/refine-shadcn) provides a streamlined approach to form validation by integrating Zod schemas with a custom Field component. This architecture leverages react-hook-form under the hood while exposing a declarative API that works seamlessly with Shadcn UI components to handle complex validation logic.

Understanding the Validation Flow

The implementation follows a five-step architecture that bridges Zod validation with UI rendering:

  1. Schema Definition – You define validation rules using z.object() in a Zod schema, specifying constraints like .min(), .email(), or .max() with custom error messages.
  2. Resolver Integration – The zodResolver adapter (from @hookform/resolvers/zod) translates Zod validation errors into the format expected by react-hook-form.
  3. Form State Initialization – useForm from @refinedev/react-hook-form receives the resolver and creates the form control object containing control, handleSubmit, and formState.
  4. Field Registration – The Field component (implemented in packages/theme/src/components/field.tsx) receives the form context via spread props and registers individual inputs using react-hook-form's Controller.
  5. Error Rendering – The Field component automatically renders FormMessage (from packages/theme/src/ui/form.tsx) which pulls validation errors from formState.errors and displays them with appropriate ARIA attributes.

Step 1: Define Your Zod Schema

Create a Zod schema that describes the shape of your form data and validation requirements. This schema enforces type safety and provides detailed error messages for each validation rule.

import * as z from "zod";

const userSchema = z.object({
    firstName: z.string().min(2, { 
        message: "Firstname must be at least 2 characters." 
    }),
    lastName: z.string().min(2, { 
        message: "Lastname must be at least 2 characters." 
    }),
    email: z.string().email({ 
        message: "Please enter a valid email address." 
    }),
});

Step 2: Connect Zod to react-hook-form

Initialize your form using useForm from @refinedev/react-hook-form and pass the zodResolver adapter with your schema. The mode: "all" configuration ensures validation runs on both change and blur events.

import { useForm } from "@refinedev/react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const UserForm = () => {
    const form = useForm<z.infer<typeof userSchema>>({
        mode: "all",
        resolver: zodResolver(userSchema),
        defaultValues: {
            firstName: "",
            lastName: "",
            email: "",
        },
    });

    const onSubmit = (data: z.infer<typeof userSchema>) => {
        console.log(data);
    };

    return (
        // Form JSX will be implemented in the next step
        <form onSubmit={form.handleSubmit(onSubmit)} />
    );
};

Step 3: Implement the Field Component

The Field component from @ferdiunal/refine-shadcn abstracts the complexity of react-hook-form's Controller. It accepts the form context via spread props and uses a render prop pattern to pass field handlers to your input components.

import { Field, Form } from "@ferdiunal/refine-shadcn";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

<Form {...form}>
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <Field {...form} name="firstName" label="Firstname">
            {(field) => <Input {...field} placeholder="Firstname" />}
        </Field>

        <Field {...form} name="lastName" label="Lastname">
            {(field) => <Input {...field} placeholder="Lastname" />}
        </Field>

        <Field {...form} name="email" label="Email">
            {(field) => <Input {...field} placeholder="Email" type="email" />}
        </Field>

        <Button type="submit">Submit</Button>
    </form>
</Form>

The Field component automatically handles:

  • Label rendering via the label prop
  • Error message display through FormMessage, which reads from formState.errors
  • Description text (optional) for helper text below inputs
  • ARIA attributes for accessibility, managed by the underlying FormField implementation

Key Source Files

Understanding the underlying implementation helps when customizing validation behavior or troubleshooting issues:

Summary

  • Zod schemas define validation rules and custom error messages using methods like .min(), .email(), and .max().

  • zodResolver bridges Zod with react-hook-form, translating schema validation errors into the format expected by the form state manager.

  • The Field component from @ferdiunal/refine-shadcn abstracts the react-hook-form Controller, automatically wiring up validation, labels, and error messages through a render prop pattern.

  • Error display happens automatically via FormMessage inside the Field component, which reads from formState.errors and renders appropriate feedback without additional configuration.

  • Source files in packages/theme/src/components/ provide the implementation details for customization, while templates/vite-react/src/pages/users/Form.tsx serves as the canonical usage example.

Frequently Asked Questions

Can I use validation libraries other than Zod with the Field component?

Yes. The Field component accepts any resolver compatible with react-hook-form, including Yup, Joi, or custom validators. Simply replace zodResolver with your preferred resolver (such as yupResolver from @hookform/resolvers/yup) and ensure your validation schema matches the expected data structure. The Field component will automatically display errors regardless of which resolver generates them.

How do I display custom error messages for specific validation rules?

Define custom messages directly in your Zod schema using the .min(), .max(), .email(), or .refine() methods with a message property. The Field component automatically passes these messages to FormMessage, which renders them when validation fails. For example: z.string().min(5, { message: "Title must be at least 5 characters" }). You can also override messages globally by customizing the FormMessage component in packages/theme/src/ui/form.tsx.

Does the Field component support nested form values or array fields?

Yes. Use dot notation for nested objects (e.g., name="profile.bio") or bracket notation for arrays (e.g., name="tags[0]"). The Field component registers these paths with react-hook-form's Controller, and Zod validates them according to your schema definition using z.object() for nested shapes or z.array() for collections. Ensure your defaultValues structure matches the nested path structure to avoid undefined errors.

How do I customize the styling of validation error messages?

The error messages render through the FormMessage component located in packages/theme/src/ui/form.tsx. You can customize the appearance by modifying the FormMessage implementation to use different Tailwind classes or by wrapping the Field component and intercepting formState.errors to render custom error UI. The default implementation uses standard Shadcn UI styling with destructive color variants (text-destructive) for error states and automatically manages ARIA attributes for accessibility.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →