web-forms-zod-validation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseZod Schema Validation Patterns
Zod Schema 验证模式
Quick Guide: A schema is declared once and the TypeScript type derived from it with, so the rule and the type cannot drift.z.inferreturns a result object where invalid input is expected andsafeParsethrows where it is a bug;parseandrefinecarry rules the built-in checks cannot express, andsuperRefineconverts during validation — which splitstransformfromz.input. On v4 the string formats moved to the top level (z.output,z.email(),z.url()) and the v3 method chains are deprecated rather than removed;z.iso.*,flatten()andformat()have replacements, and reference.md carries the full migration list.merge()
Detailed Resources:
- examples/core.md — schema definition, safe parsing, error formatting, unions, composition, async refinements
- examples/transforms.md — transforms, coercion, pipe chains, query params
- examples/advanced-patterns.md — branded types, fallbacks, readonly, recursive schemas
.catch() - reference.md — decision trees, method lookup, worked anti-patterns, v4 migration guide
快速指南: schema 只需声明一次,并通过从中派生 TypeScript 类型, 因此规则与类型不会发生偏离。对于预期会出现无效输入的场景,z.infer返回结果对象; 而在无效输入意味着存在 bug 的场景中,safeParse会抛出异常;parse和refine用于 承载内置检查无法表达的规则,superRefine则在验证期间转换值——这会使transform与z.input分离。在 v4 中,字符串格式已移至顶层(z.output、z.email()、z.url()), v3 方法链虽已弃用但尚未移除;z.iso.*、flatten()和format()均有替代方案, reference.md 列出了完整的迁移清单。merge()
详细资源:
- examples/core.md — schema 定义、安全解析、错误格式化、union、组合、异步 refinement
- examples/transforms.md — transform、coercion、pipe 链、查询参数
- examples/advanced-patterns.md — branded 类型、fallback、readonly、递归 schema
.catch() - reference.md — 决策树、方法查询、常见反模式实例、v4 迁移指南
Which path applies
适用路径
- The schema checks a value and hands back what it received — is the only type helper needed, and the parsed value has the shape the caller passed in. Follow examples/core.md.
z.infer - The schema converts as it checks — a , a
transformor acoercemakes the input and output types differ, so a function taking pre-validation data types its parameterdefaultand its returnz.input. Follow examples/transforms.md.z.output
<critical_requirements>
- schema 检查一个值并原样返回 — 只需要 这一个类型辅助工具, 解析后的值具有调用方传入时的结构。请参阅 examples/core.md。
z.infer - schema 在检查的同时进行转换 — 、
transform或coerce会使输入类型与 输出类型不同,因此接收验证前数据的函数应将参数类型设为default,返回值类型设为z.input。 请参阅 examples/transforms.md。z.output
<critical_requirements>
Before writing Zod schemas
编写 Zod schema 之前
Reach for wherever invalid input is expected. It returns a result object rather than
throwing, so the failure is a branch rather than a catch, and carries the field
paths a form needs. Keep for config and internal data, where invalid means a bug.
safeParseresult.error.issuesparseDerive the type with . A hand-written interface beside a schema is a
second declaration of the same thing, and the two drift in the direction that leaves the type
claiming more than the schema checks.
z.infer<typeof schema>Validate where untrusted data enters — API responses, form input, config, URL params. A shape
change caught at the boundary names the field that moved; caught later it surfaces as an undefined
property several frames away.
Name the validation limits. says what the number is for, and the same
constant reaches the error message so the two cannot disagree.
.min(MIN_USERNAME_LENGTH)</critical_requirements>
Auto-detection: zod, z.object, z.infer, z.input, z.output, safeParse, safeParseAsync, parse,
parseAsync, refine, superRefine, ctx.addIssue, transform, discriminatedUnion, z.coerce, z.pipe,
z.catch, z.brand, z.lazy, z.email, z.url, z.uuid, z.iso, z.flattenError, z.treeifyError,
z.strictObject, z.looseObject, ZodError
Applies to:
- Validating data crossing a trust boundary, and reporting which field failed
- Deriving TypeScript types from the rules that enforce them
- Cross-field rules, conditional shapes and discriminated variants
- Converting values during validation — strings to numbers, ISO strings to
Date - Composing schemas for the read, create and update shapes of one record
Handled elsewhere:
- Wiring a schema into a form — a form library accepts one through its own adapter or validator slot, and how that connection is made is settled by whatever owns it.
- Where the data came from — a schema validates a value it is handed and performs no I/O of its own.
- Persistence schemas — a runtime validator and a table definition are separate artefacts even where they describe the same record, and generating one from the other is that tool's concern.
<philosophy>
TypeScript checks the code you compile; a schema checks the data you receive. The two meet at the
boundary, and the point of deriving the type from the schema is that only one of them can be wrong.
typescript
const UserSchema = z.object({ name: z.string(), email: z.email() });
type User = z.infer<typeof UserSchema>;Written the other way round — an interface, and a schema maintained beside it — a field added to the
interface and forgotten in the schema type-checks everywhere while validating nothing.
The corollary is where not to reach for a schema. Data that has already crossed a boundary has
been checked, and re-validating it inside a function the compiler already governs buys nothing and
costs a parse on every call.
</philosophy>
<patterns>
只要预期输入可能无效,就使用 。 它返回结果对象而不是抛出异常,
因此失败可以通过分支处理,而无需依赖 catch; 包含表单所需的字段路径。
对于配置和内部数据应保留 ,因为这些数据无效意味着存在 bug。
safeParseresult.error.issuesparse使用 派生类型。 在 schema 旁手写 interface,相当于对同一事物
进行第二次声明,两者最终会产生偏差,导致类型声称的内容多于 schema 实际检查的内容。
z.infer<typeof schema>在不可信数据进入时进行验证 — API 响应、表单输入、配置、URL 参数。在边界处捕获结构变化,
可以明确指出发生变化的字段;若在更晚阶段才捕获,则问题会表现为若干调用栈帧之外的 undefined 属性。
为验证限制命名。 说明了数字的用途,同一常量也用于错误消息,
因此二者不会不一致。
.min(MIN_USERNAME_LENGTH)</critical_requirements>
自动检测: zod, z.object, z.infer, z.input, z.output, safeParse, safeParseAsync, parse,
parseAsync, refine, superRefine, ctx.addIssue, transform, discriminatedUnion, z.coerce, z.pipe,
z.catch, z.brand, z.lazy, z.email, z.url, z.uuid, z.iso, z.flattenError, z.treeifyError,
z.strictObject, z.looseObject, ZodError
适用场景:
- 验证跨越信任边界的数据,并报告失败的字段
- 根据实际执行验证的规则派生 TypeScript 类型
- 跨字段规则、条件结构和 discriminated variant
- 在验证期间转换值——字符串转数字、ISO 字符串转
Date - 为同一条记录的 read、create 和 update 结构组合 schema
在其他位置处理:
- 将 schema 接入表单——表单库会通过自己的 adapter 或 validator slot 接受一个 schema, 具体连接方式由相应组件决定。
- 数据的来源——schema 验证交给它的值,本身不执行 I/O。
- 持久化 schema——runtime validator 与 table definition 是两个独立构件,即使描述同一条记录也是如此; 从其中一个生成另一个属于相应工具的职责。
<philosophy>
TypeScript 检查编译的代码;schema 检查接收到的数据。二者在边界处相遇,而从 schema 派生类型的
意义在于,两者之中只有一个可能出错。
typescript
const UserSchema = z.object({ name: z.string(), email: z.email() });
type User = z.infer<typeof UserSchema>;换一种写法——先定义 interface,再在旁边维护 schema——如果 interface 中新增了字段却忘记更新 schema,
那么它仍能在各处通过类型检查,却没有验证任何内容。
推论是明确不应使用 schema 的场景。数据一旦跨越边界就已经检查过,在编译器已经管控的函数内部
再次验证它不会带来任何收益,却会为每次调用增加一次解析开销。
</philosophy>
<patterns>
Core patterns
核心模式
Pattern 1: Schema with named limits
模式 1:使用命名限制的 schema
The constant appears in the check and in the message, so a change to the limit updates both.
typescript
const MIN_USERNAME_LENGTH = 3;
const MAX_USERNAME_LENGTH = 50;
const UserSchema = z.object({
username: z
.string()
.min(
MIN_USERNAME_LENGTH,
`Username must be at least ${MIN_USERNAME_LENGTH} characters`,
)
.max(
MAX_USERNAME_LENGTH,
`Username cannot exceed ${MAX_USERNAME_LENGTH} characters`,
),
email: z.email("Invalid email format"),
});
type User = z.infer<typeof UserSchema>;Full code: examples/core.md Pattern 1
常量同时出现在检查和消息中,因此修改限制时会同步更新两者。
typescript
const MIN_USERNAME_LENGTH = 3;
const MAX_USERNAME_LENGTH = 50;
const UserSchema = z.object({
username: z
.string()
.min(
MIN_USERNAME_LENGTH,
`Username must be at least ${MIN_USERNAME_LENGTH} characters`,
)
.max(
MAX_USERNAME_LENGTH,
`Username cannot exceed ${MAX_USERNAME_LENGTH} characters`,
),
email: z.email("Invalid email format"),
});
type User = z.infer<typeof UserSchema>;完整代码:examples/core.md 模式 1
Pattern 2: safeParse and error formatting
模式 2:safeParse 与错误格式化
The result is a discriminated union on , so the failure branch narrows to an error and the
success branch to typed data.
successtypescript
const result = UserSchema.safeParse(data);
if (!result.success) {
const { fieldErrors } = z.flattenError(result.error);
return { success: false, errors: fieldErrors };
}
return { success: true, user: result.data };z.flattenErrorz.treeifyErrorFull code: examples/core.md Pattern 2
结果是以 判别的 union,因此失败分支会收窄为 error,成功分支会收窄为类型化数据。
successtypescript
const result = UserSchema.safeParse(data);
if (!result.success) {
const { fieldErrors } = z.flattenError(result.error);
return { success: false, errors: fieldErrors };
}
return { success: true, user: result.data };z.flattenErrorz.treeifyError完整代码:examples/core.md 模式 2
Pattern 3: Refinements and cross-field rules
模式 3:refinement 与跨字段规则
refinesuperRefinetypescript
const PasswordFormSchema = z
.object({
password: z.string().min(MIN_PASSWORD_LENGTH),
confirmPassword: z.string(),
})
.superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Passwords do not match",
path: ["confirmPassword"],
});
}
});Full code: examples/core.md Pattern 5
refinesuperRefinetypescript
const PasswordFormSchema = z
.object({
password: z.string().min(MIN_PASSWORD_LENGTH),
confirmPassword: z.string(),
})
.superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Passwords do not match",
path: ["confirmPassword"],
});
}
});完整代码:examples/core.md 模式 5
Pattern 4: Transforms and the input/output split
模式 4:transform 与输入/输出类型分离
Once a schema transforms, it has two types. returns the output, so a function receiving
raw data types its parameter .
z.inferz.inputtypescript
const DateSchema = z.iso.datetime().transform((str) => new Date(str));
type DateInput = z.input<typeof DateSchema>; // string
type DateOutput = z.output<typeof DateSchema>; // Date.transform().pipe()Full code: examples/transforms.md Pattern 7, and Pattern 9 for the pipe
一旦 schema 执行 transform,它就有两种类型。 返回输出类型,因此接收原始数据的函数
应将其参数类型设为 。
z.inferz.inputtypescript
const DateSchema = z.iso.datetime().transform((str) => new Date(str));
type DateInput = z.input<typeof DateSchema>; // string
type DateOutput = z.output<typeof DateSchema>; // Date.transform().pipe()完整代码:examples/transforms.md 模式 7;pipe 相关内容见模式 9
Pattern 5: Discriminated unions
模式 5:discriminated union
Where the variants share a literal field, naming it lets Zod check one branch instead of all of them.
typescript
const NotificationSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("email"), email: z.email(), subject: z.string() }),
z.object({ type: z.literal("sms"), phone: z.string(), message: z.string() }),
z.object({
type: z.literal("push"),
deviceId: z.string(),
title: z.string(),
}),
]);A plain tries every member and reports the combined failure, which reads as "invalid
input"; the discriminated form names the variant and the field inside it, and narrows in a .
z.unionswitchFull code: examples/core.md Pattern 4
当各 variant 共享一个 literal 字段时,指定该字段可让 Zod 只检查一个分支,而不是所有分支。
typescript
const NotificationSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("email"), email: z.email(), subject: z.string() }),
z.object({ type: z.literal("sms"), phone: z.string(), message: z.string() }),
z.object({
type: z.literal("push"),
deviceId: z.string(),
title: z.string(),
}),
]);普通 会尝试每个成员并报告组合后的失败信息,看起来只是“无效输入”;
而 discriminated 形式会指出具体的 variant 及其中的字段,并可在 中完成类型收窄。
z.unionswitch完整代码:examples/core.md 模式 4
Pattern 6: Composition
模式 6:组合
One base schema, and the read, create and update shapes derived from it — so a field added to the
base reaches all three.
typescript
const UserSchema = BaseEntitySchema.extend({
email: z.email(),
name: z.string(),
});
const CreateUserSchema = UserSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
});
const UpdateUserSchema = CreateUserSchema.partial();
const UserSummarySchema = UserSchema.pick({ id: true, name: true });.extend().refine()Full code: examples/core.md Pattern 3
一个基础 schema,从中派生 read、create 和 update 三种结构——这样添加到基础 schema 的字段会同步进入三者。
typescript
const UserSchema = BaseEntitySchema.extend({
email: z.email(),
name: z.string(),
});
const CreateUserSchema = UserSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
});
const UpdateUserSchema = CreateUserSchema.partial();
const UserSummarySchema = UserSchema.pick({ id: true, name: true });对于已经使用 的 schema, 不可用,因此应先 extend,最后 refine。
.refine().extend()完整代码:examples/core.md 模式 3
Pattern 7: Coercion for string sources
模式 7:针对字符串源的 coercion
URL params and form fields arrive as strings whatever they represent. converts before
checking, so the rules read as the types they are about.
z.coercetypescript
const PaginationSchema = z.object({
page: z.coerce.number().int().positive().default(DEFAULT_PAGE),
limit: z.coerce
.number()
.int()
.positive()
.max(MAX_LIMIT)
.default(DEFAULT_LIMIT),
});z.coerce.boolean()Boolean(value)true"false"z.stringbool()Full code: examples/transforms.md Pattern 8
URL 参数和表单字段无论表示什么,抵达时都是字符串。 会在检查前进行转换,
因此规则读起来与它们所描述的类型一致。
z.coercetypescript
const PaginationSchema = z.object({
page: z.coerce.number().int().positive().default(DEFAULT_PAGE),
limit: z.coerce
.number()
.int()
.positive()
.max(MAX_LIMIT)
.default(DEFAULT_LIMIT),
});z.coerce.boolean()Boolean(value)true"false"z.stringbool()完整代码:examples/transforms.md 模式 8
Pattern 8: optional, nullable, nullish and default
模式 8:optional、nullable、nullish 与 default
Four distinct claims about an absent value; picking by meaning keeps the type honest.
typescript
const ProfileSchema = z.object({
name: z.string(), // required
bio: z.string().optional(), // may be omitted — string | undefined
avatar: z.url().nullable(), // explicitly empty — string | null
nickname: z.string().nullish(), // either — string | null | undefined
theme: z.string().default("light"), // absent becomes "light" — always string
});.default()z.inputz.output<red_flags>
对于缺失值有四种不同的语义声明;按实际含义选择,可让类型保持准确。
typescript
const ProfileSchema = z.object({
name: z.string(), // required
bio: z.string().optional(), // may be omitted — string | undefined
avatar: z.url().nullable(), // explicitly empty — string | null
nickname: z.string().nullish(), // either — string | null | undefined
theme: z.string().default("light"), // absent becomes "light" — always string
});.default()z.inputz.output<red_flags>
Red flags
危险信号
Breaks at runtime:
- on user input — it throws on the expected case, and a catch block receives the exception after the branch that could have read
parsehas been skipped. Useerror.issues.safeParse - An async refinement parsed synchronously — Zod throws rather than awaiting. or
parseAsyncfor any schema containing one.safeParseAsync - on a schema that already carries
.extend()— the refined schema no longer exposes it. Extend first, refine last..refine() - v4: — the function form of the second argument is gone. Use
.refine(fn, (val) => ({ message }))where the message depends on the value.superRefine - v4: with one argument — both the key and the value schema are required now.
z.record(valueSchema)
Surprising behaviour:
- Unknown keys are stripped rather than rejected. rejects them and
z.strictObject()keeps them; the plain object schema quietly drops them, which hides a renamed API field.z.looseObject() - coerces
z.coerce.boolean()to"false", andtrueaccepts everythingz.coerce.date()does — including strings that parse to a date nobody meant. Where the input is supposed to be ISO,new Date()says so and rejects the rest.z.iso.datetime() - rejects the empty string, so an optional field that submits
z.email()fails. Allow it explicitly or normalise""to""before parsing.undefined - A rule is skipped, not failed, when a key it reads is absent — the object validates.
superRefine - Default messages reach the user unchanged wherever a check has no message.
"Invalid input" - v4: is gone from
ctx.path—superRefinestill works.ctx.addIssue({ path: [...] }) - v4 replacements: →
.flatten(),z.flattenError()→.format(),z.treeifyError()→.merge()..extend()
Worked before/after code for the most common of these is in reference.md.
</red_flags>
运行时出错:
- 对用户输入使用 ——它会在预期的无效场景中抛出异常,而本可读取
parse的分支会被跳过, catch 块只能收到异常。请使用error.issues。safeParse - 同步解析包含异步 refinement 的 schema——Zod 不会等待,而是直接抛出异常。任何包含异步
refinement 的 schema 都应使用 或
parseAsync。safeParseAsync - 对已经使用 的 schema 调用
.refine()——refine 后的 schema 不再暴露它。 应先 extend,最后 refine。.extend() - v4:——第二个参数的函数形式已移除。 当消息取决于值时,请使用
.refine(fn, (val) => ({ message }))。superRefine - v4:只有一个参数——现在必须同时提供 key schema 和 value schema。
z.record(valueSchema)
意外行为:
- 未知键会被移除,而不是被拒绝。会拒绝未知键,
z.strictObject()会保留未知键; 普通 object schema 会悄悄丢弃它们,从而掩盖 API 字段被重命名的问题。z.looseObject() - 会把
z.coerce.boolean()强制转换为"false",而true接受所有z.coerce.date()能接受的内容——包括那些会被解析成非预期日期的字符串。如果输入应当是 ISO 格式,new Date()能明确表达这一要求并拒绝其他内容。z.iso.datetime() - 会拒绝空字符串,因此提交
z.email()的可选字段会验证失败。应显式允许它, 或在解析前将""规范化为""。undefined - 当 规则读取的某个键不存在时,该规则会被跳过而不是判定失败——对象仍会验证通过。
superRefine - 当某项检查没有提供消息时,默认的 消息会原样呈现给用户。
"Invalid input" - v4:中已移除
superRefine——ctx.path仍然可用。ctx.addIssue({ path: [...] }) - v4 替代项:→
.flatten(),z.flattenError()→.format(),z.treeifyError()→.merge()。.extend()
reference.md 中提供了最常见问题的改写前后代码示例。
</red_flags>