Skip to Content

Module 6.3: Advanced Validation with Zod

Banner illustrating Zod's advanced validation capabilities with React Hook Form, showcasing a schema definition.

🎯 Lesson Objectives

After completing this lesson, you will master:

  • Schema Validation: Shift your mindset to centralized Schema Validation instead of scattered if/else checks.
  • Zod: A powerful, TypeScript-first validation library.
  • Integration: Seamlessly connect Zod with React Hook Form using @hookform/resolvers.
  • Complex Rules: Tackle challenging validation scenarios like Password Confirmation and Custom Validation.

1. Why Schema Validation?

In the previous lesson, we approached validation by passing rules directly to individual register functions. While this works fine for small forms, it quickly becomes challenging with larger ones:

  1. Validation logic becomes scattered across your tSX files.
  2. Reusing rules (e.g., an email rule needed in multiple places) is cumbersome.
  3. Cross-field validation (e.g., ensuring Confirm Password matches Password) is difficult to implement.

Zod empowers you to define all validation rules (the Schema) for your form in one centralized place.

Diagram showing how Schema Validation centralizes rules for a form.

2. Installation

You’ll need to install zod and the @hookform/resolvers adapter to integrate with React Hook Form.

npm install zod @hookform/resolvers # hoặc yarn add zod @hookform/resolvers # hoặc pnpm add zod @hookform/resolvers # hoặc bun add zod @hookform/resolvers

3. Zod Essentials (Core Concepts)

Illustration of core Zod concepts including primitives, coercion, common validations, enums, and literals.

Zod goes far beyond basic data types. Here are the core functionalities you need to master:

3.1 Primitives & Coercion

HTML input elements always return strings, even when type="number". Zod provides z.coerce to automatically cast types before validation.

const basicSchema = z.object({ // String Validation username: z.string().trim().min(3).max(20), // Number Validation & Coercion // Tự động chuyển chuỗi "25" thành số 25 age: z.coerce.number().min(18).int().positive(), // Boolean isActive: z.boolean().default(false), // Date birthDate: z.coerce.date(), });

Zod comes packed with a long list of commonly used validation rules:

  • String: .email(), .url(), .uuid(), .regex(regex), .startsWith(), .endsWith().
  • Number: .gt(5) (greater than), .gte(5) (greater than or equal), .lt(5) (less than), .lte(5) (less than or equal).
  • Optional/Nullable:
    • .optional(): Allows undefined (if not submitted).
    • .nullable(): Allows null.
    • .nullish(): Allows both null and undefined.
const profileSchema = z.object({ website: z.string().url().optional().or(z.literal('')), // URL hoặc chuỗi rỗng bio: z.string().max(100).nullish(), // Có thể null hoặc không có });

3.3 Enums & Literals (Fixed Lists)

Ideal for Dropdown (Select) or Radio Button components.

// Enums cho Select const RoleEnum = z.enum(['ADMIN', 'USER', 'GUEST']); type Role = z.infer<typeof RoleEnum>; // 'ADMIN' | 'USER' | 'GUEST' // Literals cho giá trị chính xác const termsSchema = z.object({ agreed: z.literal(true, { errorMap: () => ({ message: 'Agreement is required' }), }), });

4. Zod Advanced Patterns (Advanced Techniques)

4.1 Refine (Custom Validation)

When built-in rules aren’t enough, .refine() is your go-to. It’s Zod’s most powerful tool for crafting custom validation logic.

const passwordSchema = z.string().refine((val) => !val.includes('123456'), { message: "Password cannot contain the string '123456'", }); // Async Refine (Kiểm tra Server) const emailSchema = z .string() .email() .refine( async (email) => { const exists = await checkEmailExists(email); return !exists; }, { message: 'Email is already in use', } );

4.2 Cross-Field Validation

Use .refine() or .superRefine() at the object level to compare fields with each other (e.g., Confirm Password, Start Date < End Date).

const changePassSchema = z .object({ password: z.string(), confirm: z.string(), }) .refine((data) => data.password === data.confirm, { message: 'Passwords do not match', path: ['confirm'], // Chỉ định lỗi hiển thị ở trường 'confirm' });

4.3 Transformations (Data Manipulation)

Use .transform() to modify the output data after successful validation.

const dataSchema = z .object({ // Tự động viết hoa tên khi submit name: z.string().transform((val) => val.toUpperCase()), // Tính toán giá trị mới price: z.number(), quantity: z.number(), }) .transform((data) => ({ ...data, total: data.price * data.quantity, // Thêm trường total tự động }));

4.4 Error Handling

Customize error messages, either globally or for specific fields.

// Cách 1: Custom message inline z.string().min(1, { message: 'This field cannot be empty' }); // Cách 2: Custom Error Map (Toàn cục) // (Thường dùng trong file setup của dự án) z.setErrorMap((issue, ctx) => { if (issue.code === z.ZodIssueCode.invalid_type) { return { message: 'Invalid data type' }; } return { message: ctx.defaultError }; });

5. Integrating Zod with React Hook Form

Leverage zodResolver within useForm.

import { useForm, type SubmitHandler } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const signUpSchema = z.object({ fullName: z.string().min(1, 'Full name is required'), email: z.string().email('Invalid email format'), password: z.string().min(6, 'Password must be at least 6 characters'), }); type SignUpFormValues = z.infer<typeof signUpSchema>; export default function SignUpForm() { const { register, handleSubmit, formState: { errors }, } = useForm<SignUpFormValues>({ resolver: zodResolver(signUpSchema), }); const onSubmit: SubmitHandler<SignUpFormValues> = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)} className="p-4 space-y-4"> <div> <input {...register('fullName')} className="border p-2 w-full" placeholder="Full Name" /> {errors.fullName && <p className="text-red-500">{errors.fullName.message}</p>} </div> <div> <input {...register('email')} className="border p-2 w-full" placeholder="Email" /> {errors.email && <p className="text-red-500">{errors.email.message}</p>} </div> <div> <input type="password" {...register('password')} className="border p-2 w-full" placeholder="Password" /> {errors.password && <p className="text-red-500">{errors.password.message}</p>} </div> <button type="submit" className="bg-blue-600 text-white p-2 rounded"> Sign Up </button> </form> ); }

6. Hands-On Labs

Lab 1: Tactical Access (Security Setup)

Build a security system with a Tactical Industrial design language (High Contrast).

  • Features:
    • Entropy Monitor: A password strength meter using a “Segmented Bar” (Industrial Style).
    • Mechanical Toggle: A mechanical toggle switch to replace standard checkboxes.
    • Strict Validation: Instant error feedback with a warning UI.
  • Techniques: Employ z.boolean().refine() for the toggle, useWatch to calculate strength, and pure CSS for the mechanical elements.

A tactical access security form with a segmented password strength meter and a mechanical toggle switch.

💡 Tips:

  • To compare password and confirmPassword, use .refine() at the top-level of your Schema (the object level).
  • For the “Accept Terms” checkbox, Zod defaults to a boolean; use .refine(val => val === true) to enforce selection.
  • Extract the StrengthMeter into a separate component and use useWatch to independently monitor the password field, preventing unnecessary full form re-renders.

Lab 2: Swiss Invoice (Minimalist Document)

Create an invoice generator with a Swiss Design aesthetic (Typography-centric), embodying absolute minimalism.

  • Features:
    • Editorial Preview: A preview interface that resembles a high-end magazine page or printed document.
    • Realtime Calculation: Automatically calculate the total amount as quantity or unit price changes.
    • Dynamic Items: Leverage useFieldArray (covered in Module 6.2) for automatic row addition/removal.
  • Techniques: useFieldArray combined with a zod array schema, and watch to calculate data.

A minimalist invoice generator form in Swiss Design, featuring dynamic items and real-time calculations.

💡 Tips:

  • Define your schema using z.array() for your items.
  • Use useWatch to extract quantity and price values for total calculations.
  • For a valid date string from an input type="date", a simple z.string() validation is sufficient.

7. Resources

Last updated on