关于 web-forms-zod-validation
面向 TypeScript 开发者的 Zod 模式编写指南,涵盖数据验证、类型推导、转换、组合及 v4 迁移,适用于在应用边界校验数据。
主要功能
- 01安全解析与错误格式化
- 02使用 z.infer 推导类型
- 03细化规则与跨字段校验
- 04转换及输入输出类型
- 05模式组合与联合类型
- 06Zod v4 迁移指引
适用场景
- 01校验表单输入并报告字段错误
- 02检查 API 响应和 URL 参数
- 03表达条件变体与跨字段规则
- 04将字符串转换为数字或日期
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-forms-zod-validation安装后在 Claude Code 中输入/web-forms-zod-validation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/advanced-patterns.mdcore.mdtransforms.md详细介绍
Zod 模式验证模式
快速指南: 只需声明一次模式,再用
z.infer推导 TypeScript 类型,这样规则和类型就不会出现偏差。当无效输入属于预期情况时,safeParse会返回结果对象;当出现错误就意味着代码有问题时,parse会抛出异常。refine和superRefine可用于编写内置检查无法表达的规则,而transform会在验证期间转换数据,因此z.input和z.output会有所不同。在 v4 中,字符串格式方法移到了顶层(z.email()、z.url()、z.iso.*),v3 的方法链已弃用,但尚未移除;flatten()、format()和merge()均有替代方法,完整迁移列表见 reference.md。
详细资源:
- examples/core.md — 模式定义、安全解析、错误格式化、联合类型、组合、异步细化
- examples/transforms.md — 转换、强制转换、管道链、查询参数
- examples/advanced-patterns.md — 品牌类型、
.catch()回退、只读、递归模式 - reference.md — 决策树、方法查找、反模式示例、v4 迁移指南
适用路径
- 模式检查值并返回收到的值 — 只需使用
z.infer这一个类型辅助工具,解析后的值与调用方传入的值结构相同。请参阅 examples/core.md。 - 模式在检查时进行转换 —
transform、coerce或default会导致输入和输出类型不同,因此,接收验证前数据的函数应将参数类型标为z.input,并将返回值类型标为z.output。请参阅 examples/transforms.md。
<critical_requirements>
编写 Zod 模式前
预期会收到无效输入时,请优先使用 safeParse。 它返回结果对象而不是抛出异常,因此失败会进入一个分支,而不是 catch;result.error.issues 则包含表单所需的字段路径。对于配置和内部数据,请使用 parse,因为无效数据意味着存在错误。
使用 z.infer<typeof schema> 推导类型。 在模式旁手写接口,就相当于对同一事物声明两次;两者会逐渐偏离,最终类型声称的内容比模式实际检查的更多。
在不可信数据进入系统时进行验证——例如 API 响应、表单输入、配置和 URL 参数。在边界处捕获形状变化,可以指出发生变化的字段;如果之后才捕获,就会在几层调用之外暴露为未定义属性。
为验证限制命名。 .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 类型
- 跨字段规则、条件形状和判别变体
- 在验证期间转换值——例如将字符串转为数字、将 ISO 字符串转为
Date - 为同一条记录的读取、创建和更新形状组合模式
其他部分负责:
- 将模式接入表单——表单库可通过自己的适配器或验证器接口接收模式;具体连接方式由负责该部分的组件决定。
- 数据的来源——模式会验证传入的值,本身不会进行 I/O。
- 持久化模式——运行时验证器和表定义是不同的产物,即使它们描述的是同一条记录也是如此;如何根据其中一个生成另一个,属于相应工具的职责。
TypeScript 会检查编译的代码;模式会检查接收的数据。两者在边界处相遇,而从模式推导类型的意义就在于,只有其中一个可能出错。
const UserSchema = z.object({ name: z.string(), email: z.email() });
type User = z.infer<typeof UserSchema>;
反过来写——先定义接口,再在旁边维护模式——如果在接口中添加字段却忘了更新模式,代码会在所有地方通过类型检查,却不会验证任何内容。
由此可知,哪些地方_不该_使用模式。已经跨越边界的数据经过了检查;在编译器已能约束的函数内部重新验证它没有任何收益,却会在每次调用时增加一次解析。
核心模式
模式 1:使用命名限制的模式
常量同时出现在检查条件和消息中,因此修改限制时,两处都会同步更新。
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
模式 2:safeParse 和错误格式化
结果是按 success 区分的联合类型,因此失败分支会收窄为错误,成功分支则会收窄为类型化数据。
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.flattenError 会生成一层字段到消息的映射,适用于扁平表单。
z.treeifyError 则会保留更深层的嵌套结构。
完整代码:examples/core.md 模式 2
模式 3:细化与跨字段规则
refine 会为单个值添加条件。superRefine 可以查看整个对象,并选择错误附加到哪个路径,因此不匹配消息会显示在用户需要修正的字段上。
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
模式 4:转换与输入/输出类型的区分
架构经过转换后会有两种类型。z.infer 返回输出类型,因此接收原始数据的函数应将参数标注为 z.input。
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
模式 5:判别联合
如果各个变体共享一个字面量字段,为其命名后,Zod 就能检查对应分支,而不是检查所有分支。
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(),
}),
]);
普通的 z.union 会尝试每个成员并报告合并后的失败信息,显示为“无效输入”;判别形式会命名变体及其内部字段,并在 switch 中进行类型收窄。
完整代码:examples/core.md 模式 4
模式 6:组合
使用一个基础架构,并由它派生出读取、创建和更新的形状,这样添加到基础架构中的字段就会应用到这三种形状。
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 });
已包含 .refine() 的架构无法使用 .extend(),因此应先扩展,最后再进行精细校验。
完整代码:examples/core.md 模式 3
模式 7:将字符串来源的值强制转换为所需类型
URL 参数和表单字段无论表示什么,传入时都是字符串。z.coerce 会在检查前进行转换,因此规则可以按其实际类型来编写。
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
模式 8:optional、nullable、nullish 和 default
这四种方式对缺失值表达了不同含义;根据实际含义选择,才能确保类型准确。
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.input 和 z.output 不同的原因。
<red_flags>
注意事项
运行时会出错:
- 对用户输入使用
parse:在预期情况下会抛出异常,而且 catch 块会在可能读取error.issues的分支被跳过后才收到异常。请使用safeParse。 - 对包含异步精细校验的架构进行同步解析:Zod 会抛出异常,而不会等待。对于包含异步精细校验的架构,请使用
parseAsync或safeParseAsync。 - 对已包含
.refine()的架构使用.extend():精细校验后的架构不再提供该方法。应先扩展,最后再进行精细校验。 - v4:
.refine(fn, (val) => ({ message })):第二个参数的函数形式已移除。若消息取决于值,请使用superRefine。 - v4:使用单个参数的
z.record(valueSchema):现在键和值的架构都必需。
行为出乎意料:
- 未知键会被剥离,而不是拒绝。
z.strictObject()会拒绝这些键,z.looseObject()会保留它们;普通对象架构会悄悄丢弃这些键,从而隐藏被重命名的 API 字段。 z.coerce.boolean()会将"false"强制转换为true,而z.coerce.date()会接受new Date()能接受的所有值,包括能解析为日期、但并非预期的字符串。如果输入应为 ISO 格式,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>