zod-validation-utilities

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Zod Validation Utilities

Zod 校验工具

Overview

概述

Production-ready Zod v4 patterns for reusable, type-safe validation with minimal boilerplate. Focuses on modern APIs, predictable error handling, and form integration.
可用于生产环境的Zod v4模式,实现可复用、类型安全的校验能力,同时减少样板代码。专注于现代API、可预测的错误处理和表单集成。

When to Use

适用场景

  • Defining request/response validation schemas in TypeScript services
  • Parsing untrusted input from APIs, forms, env vars, or external systems
  • Standardizing coercion, transforms, and cross-field validation
  • Building reusable schema utilities across teams
  • Integrating React Hook Form with Zod using
    zodResolver
  • 在TypeScript服务中定义请求/响应校验schema
  • 解析来自API、表单、环境变量或外部系统的不可信输入
  • 统一类型强制转换、数据转换逻辑和跨字段校验
  • 跨团队构建可复用的schema工具
  • 借助
    zodResolver
    实现React Hook Form与Zod的集成

Instructions

使用说明

  1. Start with strict object schemas and explicit field constraints
  2. Prefer modern Zod v4 APIs and the
    error
    option for error messages
  3. Use coercion at boundaries (
    z.coerce.*
    ) when input types are uncertain
  4. Keep business invariants in
    refine
    /
    superRefine
    close to schema definitions
  5. Export both schema and inferred types (
    z.input
    /
    z.output
    ) for consistency
  6. Reuse utility schemas (email, id, dates, pagination) to reduce duplication
  1. 从严格的对象schema和明确的字段约束开始定义
  2. 优先使用现代Zod v4 API,通过
    error
    选项自定义错误消息
  3. 当输入类型不确定时,在边界处使用类型强制转换(
    z.coerce.*
  4. 将业务不变规则放在
    refine
    /
    superRefine
    中,尽量靠近schema定义
  5. 同时导出schema和推断出的类型(
    z.input
    /
    z.output
    )以保证一致性
  6. 复用通用工具类schema(邮箱、ID、日期、分页)减少重复代码

Examples

示例

1) Modern Zod 4 primitives and object errors

1) 现代Zod 4基础类型与对象错误

ts
import { z } from "zod";

export const UserIdSchema = z.uuid({ error: "Invalid user id" });
export const EmailSchema = z.email({ error: "Invalid email" });
export const WebsiteSchema = z.url({ error: "Invalid URL" });

export const UserProfileSchema = z.object(
  {
    id: UserIdSchema,
    email: EmailSchema,
    website: WebsiteSchema.optional(),
  },
  { error: "Invalid user profile payload" }
);
ts
import { z } from "zod";

export const UserIdSchema = z.uuid({ error: "Invalid user id" });
export const EmailSchema = z.email({ error: "Invalid email" });
export const WebsiteSchema = z.url({ error: "Invalid URL" });

export const UserProfileSchema = z.object(
  {
    id: UserIdSchema,
    email: EmailSchema,
    website: WebsiteSchema.optional(),
  },
  { error: "Invalid user profile payload" }
);

2) Coercion, preprocess, and transform

2) 类型强制转换、预处理与数据转换

ts
import { z } from "zod";

export const PaginationQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
  includeArchived: z.coerce.boolean().default(false),
});

export const DateFromUnknownSchema = z.preprocess(
  (value) => (typeof value === "string" || value instanceof Date ? value : undefined),
  z.coerce.date({ error: "Invalid date" })
);

export const NormalizedEmailSchema = z
  .string()
  .trim()
  .toLowerCase()
  .email({ error: "Invalid email" })
  .transform((value) => value as Lowercase<string>);
ts
import { z } from "zod";

export const PaginationQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
  includeArchived: z.coerce.boolean().default(false),
});

export const DateFromUnknownSchema = z.preprocess(
  (value) => (typeof value === "string" || value instanceof Date ? value : undefined),
  z.coerce.date({ error: "Invalid date" })
);

export const NormalizedEmailSchema = z
  .string()
  .trim()
  .toLowerCase()
  .email({ error: "Invalid email" })
  .transform((value) => value as Lowercase<string>);

3) Complex schema structures

3) 复杂schema结构

ts
import { z } from "zod";

const TagSchema = z.string().trim().min(1).max(40);

export const ProductSchema = z.object({
  sku: z.string().min(3).max(24),
  tags: z.array(TagSchema).max(15),
  attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
  dimensions: z.tuple([z.number().positive(), z.number().positive(), z.number().positive()]),
});

export const PaymentMethodSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("card"), last4: z.string().regex(/^\d{4}$/) }),
  z.object({ type: z.literal("paypal"), email: z.email() }),
  z.object({ type: z.literal("wire"), iban: z.string().min(10) }),
]);
ts
import { z } from "zod";

const TagSchema = z.string().trim().min(1).max(40);

export const ProductSchema = z.object({
  sku: z.string().min(3).max(24),
  tags: z.array(TagSchema).max(15),
  attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
  dimensions: z.tuple([z.number().positive(), z.number().positive(), z.number().positive()]),
});

export const PaymentMethodSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("card"), last4: z.string().regex(/^\d{4}$/) }),
  z.object({ type: z.literal("paypal"), email: z.email() }),
  z.object({ type: z.literal("wire"), iban: z.string().min(10) }),
]);

4)
refine
and
superRefine

4)
refine
superRefine
用法

ts
import { z } from "zod";

export const PasswordSchema = z
  .string()
  .min(12)
  .refine((v) => /[A-Z]/.test(v), { error: "Must include an uppercase letter" })
  .refine((v) => /\d/.test(v), { error: "Must include a number" });

export const RegisterSchema = z
  .object({
    email: z.email(),
    password: PasswordSchema,
    confirmPassword: z.string(),
  })
  .superRefine((data, ctx) => {
    if (data.password !== data.confirmPassword) {
      ctx.addIssue({
        code: "custom",
        path: ["confirmPassword"],
        message: "Passwords do not match",
      });
    }
  });
ts
import { z } from "zod";

export const PasswordSchema = z
  .string()
  .min(12)
  .refine((v) => /[A-Z]/.test(v), { error: "Must include an uppercase letter" })
  .refine((v) => /\d/.test(v), { error: "Must include a number" });

export const RegisterSchema = z
  .object({
    email: z.email(),
    password: PasswordSchema,
    confirmPassword: z.string(),
  })
  .superRefine((data, ctx) => {
    if (data.password !== data.confirmPassword) {
      ctx.addIssue({
        code: "custom",
        path: ["confirmPassword"],
        message: "Passwords do not match",
      });
    }
  });

5) Optional, nullable, nullish, and default

5) 可选、可空、nullish与默认值

ts
import { z } from "zod";

export const UserPreferencesSchema = z.object({
  nickname: z.string().min(2).optional(),      // undefined allowed
  bio: z.string().max(280).nullable(),         // null allowed
  avatarUrl: z.url().nullish(),                // null or undefined allowed
  locale: z.string().default("en"),           // fallback when missing
});
ts
import { z } from "zod";

export const UserPreferencesSchema = z.object({
  nickname: z.string().min(2).optional(),      // undefined allowed
  bio: z.string().max(280).nullable(),         // null allowed
  avatarUrl: z.url().nullish(),                // null or undefined allowed
  locale: z.string().default("en"),           // fallback when missing
});

6) React Hook Form integration (
zodResolver
)

6) React Hook Form集成(
zodResolver

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

const ProfileFormSchema = z.object({
  name: z.string().min(2, { error: "Name too short" }),
  email: z.email({ error: "Invalid email" }),
  age: z.coerce.number().int().min(18),
});

type ProfileFormInput = z.input<typeof ProfileFormSchema>;
type ProfileFormOutput = z.output<typeof ProfileFormSchema>;

const form = useForm<ProfileFormInput, unknown, ProfileFormOutput>({
  resolver: zodResolver(ProfileFormSchema),
  criteriaMode: "all",
});
tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const ProfileFormSchema = z.object({
  name: z.string().min(2, { error: "Name too short" }),
  email: z.email({ error: "Invalid email" }),
  age: z.coerce.number().int().min(18),
});

type ProfileFormInput = z.input<typeof ProfileFormSchema>;
type ProfileFormOutput = z.output<typeof ProfileFormSchema>;

const form = useForm<ProfileFormInput, unknown, ProfileFormOutput>({
  resolver: zodResolver(ProfileFormSchema),
  criteriaMode: "all",
});

Best Practices

最佳实践

  • Keep schemas near boundaries (HTTP handlers, queues, config loaders)
  • Prefer
    safeParse
    for recoverable flows;
    parse
    for fail-fast execution
  • Share small schema utilities (
    id
    ,
    email
    ,
    slug
    ) to enforce consistency
  • Use
    z.input
    and
    z.output
    when transforms/coercions change runtime shape
  • Avoid overusing
    preprocess
    ; prefer explicit
    z.coerce.*
    where possible
  • Treat external payloads as untrusted and always validate before use
  • 将schema放在靠近边界的位置(HTTP handler、队列、配置加载器)
  • 可恢复流程优先使用
    safeParse
    ,需要快速失败的执行场景使用
    parse
  • 共享小型schema工具(
    id
    email
    slug
    )以保证规则一致性
  • 当转换/类型强制修改了运行时结构时,使用
    z.input
    z.output
  • 避免过度使用
    preprocess
    ,尽可能优先使用显式的
    z.coerce.*
  • 将外部负载视为不可信,使用前务必进行校验

Constraints and Warnings

约束与警告

  • Ensure examples match your installed
    zod
    major version (v4 APIs shown)
  • error
    is the preferred option for custom errors in Zod v4 patterns
  • Discriminated unions require a stable discriminator key across variants
  • Coercion can hide bad upstream data; add bounds and refinements defensively
  • 确保示例与你安装的
    zod
    主版本匹配(此处展示的是v4 API)
  • 在Zod v4模式中,
    error
    是自定义错误的推荐选项
  • 可辨识联合要求所有变体都有稳定的辨识键
  • 类型强制转换可能会隐藏上游不良数据,请防御性地添加边界和校验规则