zod-validation-utilities
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseZod 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工具
- 借助实现React Hook Form与Zod的集成
zodResolver
Instructions
使用说明
- Start with strict object schemas and explicit field constraints
- Prefer modern Zod v4 APIs and the option for error messages
error - Use coercion at boundaries () when input types are uncertain
z.coerce.* - Keep business invariants in /
refineclose to schema definitionssuperRefine - Export both schema and inferred types (/
z.input) for consistencyz.output - Reuse utility schemas (email, id, dates, pagination) to reduce duplication
- 从严格的对象schema和明确的字段约束开始定义
- 优先使用现代Zod v4 API,通过选项自定义错误消息
error - 当输入类型不确定时,在边界处使用类型强制转换()
z.coerce.* - 将业务不变规则放在/
refine中,尽量靠近schema定义superRefine - 同时导出schema和推断出的类型(/
z.input)以保证一致性z.output - 复用通用工具类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
refinesuperRefine4) refine
与superRefine
用法
refinesuperRefinets
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
)
zodResolver6) React Hook Form集成(zodResolver
)
zodResolvertsx
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 for recoverable flows;
safeParsefor fail-fast executionparse - Share small schema utilities (,
id,email) to enforce consistencyslug - Use and
z.inputwhen transforms/coercions change runtime shapez.output - Avoid overusing ; prefer explicit
preprocesswhere possiblez.coerce.* - Treat external payloads as untrusted and always validate before use
- 将schema放在靠近边界的位置(HTTP handler、队列、配置加载器)
- 可恢复流程优先使用,需要快速失败的执行场景使用
safeParseparse - 共享小型schema工具(、
id、email)以保证规则一致性slug - 当转换/类型强制修改了运行时结构时,使用和
z.inputz.output - 避免过度使用,尽可能优先使用显式的
preprocessz.coerce.* - 将外部负载视为不可信,使用前务必进行校验
Constraints and Warnings
约束与警告
- Ensure examples match your installed major version (v4 APIs shown)
zod - is the preferred option for custom errors in Zod v4 patterns
error - Discriminated unions require a stable discriminator key across variants
- Coercion can hide bad upstream data; add bounds and refinements defensively
- 确保示例与你安装的主版本匹配(此处展示的是v4 API)
zod - 在Zod v4模式中,是自定义错误的推荐选项
error - 可辨识联合要求所有变体都有稳定的辨识键
- 类型强制转换可能会隐藏上游不良数据,请防御性地添加边界和校验规则