Zod Schema
当任何变更涉及 Zod 时,请使用此能力——无论是编写 schema、放置解析调用、解码负载、还是处理错误。它拥有 schema 层:形状、解析调用、形状产生的类型、产生的错误,以及在网络格式和领域模型之间传输数据的编解码器。
它不拥有四个相邻的问题,每个问题由各自的能力负责:
- 输入是否根本不受信任,因此必须被检查——这是应用安全能力做出的判断。此技能假定该判断已做出,并说明 Zod 如何执行它。
- 存在一个钩子供解析调用放置于其中——路由处理器、服务端函数、表单操作——这是框架能力的事实。
- 获取的负载何时被重新获取、缓存或失效——这是服务端状态能力的关注点。此技能仅在被其拥有的查询函数内部放入解析调用。
- 组件如何接收、渲染和针对解析结果进行测试——这是组件开发能力的关注点。
当此处的规则与上述任何一项有对应关系时,此技能会陈述 Zod 机制并指定其他方为所有者。
版本规范。 此技能仅针对 Zod 4 编写。Zod 3 作为支持的版本线已超出范围:无迁移路径,无 zod/v3 指南,且未以其 Zod 3 形式陈述任何规则。Zod 3 出现的唯一位置是作为检测信号——一份过时惯用法的列表,以便识别过时的代码并替换为当前形式。这是一个关于 Zod 4 代码库的 Zod 4 规则,而非对 Zod 3 的覆盖。这个边界此时比平时更重要,因为跨教程、论坛帖子和模型召回,Zod 3 的答案仍然优于 Zod 4 的答案——并且大多数 Zod 3 惯用法在 Zod 4 安装下仍然可以编译,因此“它能运行”不能证明规则是当前的。Zod 4 在其主要版本内也在变动:z.xor、z.invertCodec 和 .brand 方向参数都是在 4.0 之后才出现的。这里每个对版本敏感的陈述都指明了其验证的目标,并且已知会变动的表面上的规则是查找——查阅已安装版本的官方文档 https://zod.dev/——而不是一个固定的 API 名称。将此技能或其他任何地方关于 Zod API 的未经版本限定的声明视为可疑。
已针对 [email protected](发布于 2026-05-04)验证——https://zod.dev/——前提条件为 TypeScript 5.5+ 和 strict: true。每个参考文件都带有其自己的“已针对”行,表明其规则所依据的上游页面。
超出范围。 任何形式的 Zod 3。4.5.0-canary 版本线,没有规则是基于它推导的。其他验证库,除非其与一个共享边界——覆盖了 Standard Schema 的互操作,但不覆盖 Valibot 或 ArkType 自身的 API。对于发出 Zod schema 的代码生成工具,它们被提及是为了说明它们改变了哪些你仍需手写的内容,但其自身的配置还是归它们所有。
本文件中的关键词“必须”、“不得”、“需要”、“应”、“不应”、“推荐”、“可以”和“可选”应按照 RFC 2119 中的描述进行解释。
验证边界
请参阅 validation-boundary.md 了解:
- 在数据进入的边缘进行一次解析,并且在其内部从不重新解析
- 解析上游的
unknown和下游的z.infer<typeof Schema>,绝不使用手写接口 - 哪些问题属于应用安全、框架、服务端状态或组件能力
- schema 带来成本而无收益的情况——内部数据、已解析的值、逐项循环
- 在 Zod 旁边运行第二个验证器,并在一个地方进行转换
版本与包
请参阅 version-and-packages.md 了解:
- 在信任页面之前区分
zod.dev(Zod 4)和v3.zod.dev(Zod 3) - 在
zod、zod/mini、zod/locales和zod/v4/*永久链接之间进行选择 - 假设的 TypeScript 5.5+ 和
strict: true,没有它们,推断出的可选性是不健全的 z.xor、z.invertCodec、.safeExtend、z.property和.brand方向的次版本底线- 仍然在 Zod 4 下编译的 Zod 3 惯用法,作为过时信号
Schema 模块
请参阅 schema-modules.md 了解:
const X/type X和zX的命名约定,以及为每个仓库选择一个- 共享的
EntityId、Timestamp和NonEmptyString原语,作为其他 schema 组合的词汇表 .brand<"Name">()、其"out"/"in"/"inout"方向,以及强制通过解析的 fixtures- 使用
.shape、.extend、.safeExtend、.pick、.omit以及.merge()在 4.4 中对 refinements 的抛出进行派生 - 将每个 schema 提升到模块作用域,而不是每次请求或渲染时重新构建
原语与格式
请参阅 primitives-and-formats.md 了解:
- 取代了
z.string().x()方法的顶级z.email()、z.url()、z.uuid()和z.iso.datetime() - 选择严格的
z.uuid()而非宽松的z.guid(),以及选择z.httpUrl()而非z.url()来处理获取的 URL - 为项目自身格式使用
z.stringFormat(),为模板字面量类型使用z.templateLiteral() z.int()的安全整数范围、对Infinity的拒绝,以及固定宽度的z.int32()和z.float64()- 优先使用
z.enum()而非已弃用的z.nativeEnum(),以及.exclude()和.extract()
对象与集合
请参阅 objects-and-collections.md 了解:
z.object()会剥离未建模的键,以及z.strictObject()、z.looseObject()和.catchall()如何改变这一行为- 通过 getter 进行自引用,以及循环性错误所需的返回类型注解
z.record()的两个必需参数、详尽的枚举键,以及z.partialRecord()z.tuple()中.nonempty()不再产生元组类型,以及z.file()和z.json()- 解析一次数组 schema 而不是对每个元素解析一项 schema
联合
请参阅 unions.md 了解:
- 使用
z.discriminatedUnion()进行窄化、提升错误质量以及比普通z.union()更快的解析速度 - 每个分支必须固定的字面量判别器,以及 Zod 4 允许在其内部使用的嵌套联合和管道
- 当匹配两个分支的值是一个缺陷时,使用
z.xor() z.intersection()在无法合并的结果上抛出普通的Error而非ZodError- 本应是一个对象的可选字段形状的联合
可选性与默认值
请参阅 optionality-and-defaults.md 了解:
- 根据生产者实际发出的内容,选择
.optional()、.nullable()或.nullish() .default()在输出类型上短路,而.prefault()在输入类型上解析.catch()将解析失败转为回退值,以及何时这隐藏了一个漂移的生产者- 使用
.nullish().transform((v) => v ?? null)来折叠混合了null和undefined的生产者 exactOptionalPropertyTypes对可选属性推断类型的影响
细化
请参阅 refinements.md 了解:
.refine()、.superRefine()和.check(),以及它们都不得抛出的规则ctx.addIssue()、z.NEVER以及将错误定位到特定字段的path- 使用
abort停止链,以及 4.4 的when跳过输入已无效的检查 - 异步细化强制组合 schema 的每个现有调用者使用
.parseAsync() - 属于领域而非 schema 的权限、配额或唯一性规则
转换与管道
请参阅 transforms-and-pipes.md 了解:
- 验证后的
.transform()、验证前的z.preprocess(),以及两个 schema 之间的.pipe() - 值改变但类型及其可内省性不得改变时的
.overwrite() z.input与z.output/z.infer,以及混淆它们导致的表单和 fixture 错误- 使用
ctx.issues.push或 4.4 中的ctx.addIssue配合z.NEVER从转换内部失败 - 重塑生产者数据的结束点和领域决策的开始点
编解码器
请参阅 codecs.md 了解:
- 为
.transform()无法逆转的边界使用z.codec(input, output, { decode, encode }) .decode()和.encode()及其safe*和*Async形式,以及z.invertCodec()- 已记录的
isoDatetimeToDate、json、stringToURL和base64ToBytes模板——复制,而非导入 - 默认值和
.catch()仅应用于解码,而.transform()使编码抛出非ZodError - 不会失败的总编解码器,以及因此它不提供的形状保证
强制转换与配置
请参阅 coercion-and-config.md 了解:
z.coerce.number().parse("")返回0,以及z.coerce.boolean().parse("false")返回truez.stringbool()作为拒绝既不在真值集也不在假值集中的值的构造- 查询字符串、表单数据和环境变量作为三个字符串值来源
- 在启动时一次性解析配置,并终止而非稍后返回
undefined - 将密钥配置排除在日志和客户端包之外
解析
请参阅 parsing.md 了解:
- 当失败是缺陷时使用
.parse(),当失败是需要分支的结果时使用.safeParse() - 一旦 schema 中的任何检查是异步的,
.parseAsync()就是强制性的而非可选的 - 解析返回的深拷贝,以及由此打破的引用同一性
- 将
ZodError重新映射为领域错误,以便 Zod 不进入领域签名 - 记录边界和问题路径,绝不记录失败的输入
错误
请参阅 errors.md 了解:
- 包含
code和path的error.issues,以及已丢弃的.errors和.formErrors别名 - 由消费者选择的
z.treeifyError()、z.flattenError()和z.prettifyError() - 取代了
message、invalid_type_error和errorMap的统一error参数 - 在 Zod 4 中反转的优先级——schema、每次解析、
z.config({ customError })、locale reportInput将不受信任的输入放入会传递到跟踪器或客户端的错误中
元数据与 JSON Schema
请参阅 metadata-and-json-schema.md 了解:
.meta()、z.registry()、z.globalRegistry,以及通过声明合并扩展GlobalMeta- 元数据绑定到 schema 实例,因此
.optional()或.extend()会孤立它 z.toJSONSchema()的target、io、unrepresentable、cycles、reused和uri选项io: "input"产生与默认输出视图不同的文档- 没有 JSON Schema 表示的
date、map、set、transform和bigint类型
模型结构化输出
请参阅 model-structured-output.md 了解:
- 将 schema 作为
generateObject或工具契约,以及在需要原始形状时使用satisfies z.ZodRawShape .describe()文本作为指令而非文档到达模型- 在提供者的严格模式下,
.nullable()能够存活,而.optional()和.nullish()被拒绝 - 重用
.shape.field,以使模型输出不会偏离其供给的领域类型 - 在有限重试次数下从
ZodError重新提示,而不是修补无效输出
表单
请参阅 forms.md 了解:
- 对于 Zod 自身的详细问题,使用 Zod 特定解析器;对于库独立性,使用 Standard Schema 解析器
- 使用
z.input获取表单的值和默认值,使用z.output获取提交处理器 - 跨字段细化上的
path是错误到达正确输入的唯一途径 - 空白数字字段提交的空字符串,以及未选中复选框发送的缺失键
- 客户端 schema 永远无法替代的服务端重新解析
数据存储边界
请参阅 data-store-boundaries.md 了解:
- 存储形状和领域模型作为两个 schema,它们之间有一个转换
- 读写两半作为一个配对编写,以便重命名的字段不会在它们之间漂移
- 对于补丁使用
.partial(),对于携带存储哨兵创建的输入使用.omit().extend() - 对于驱动程序自身类型使用
z.instanceof(),对于约束其字段之一使用z.property() - 负载随查询深度、草稿状态或语言环境而变化——以及这不是答案的全可选 schema
性能与占用空间
请参阅 performance-and-footprint.md 了解:
- 已发布的约 14 倍、约 7 倍和约 6.5 倍的解析性能提升,以及约 25,000 到约 175 的类型实例化下降
zod/mini的函数式 API 和 1.88 kB 核心,以及为什么服务器不应采用它- 从热点路径中提升的 schema 构建,以及一次解析集合而非每个项目一次
- 在测量的路径上,优先使用
.check()而非.refine(),以及优先使用判别联合而非线性联合 z.looseObject()跳过键剥离过程,以及因此而放弃的保护
安全态势
请参阅 security-posture.md 了解:
z.object()的剥离作为批量赋值保护,以及z.looseObject()重新允许了什么- 通过解析并不是 HTML 转义、SQL 参数化或安全获取保证
- 将
.max()放在.regex()或昂贵细化之前,以限制攻击者控制的工作 - 无界数组、无界递归深度以及永远循环的循环输入
- 一个白名单响应 schema,没有它,私有字段会随负载一起离开
测试 Schema
请参阅 testing-schemas.md 了解:
- 为什么断言
z.email()拒绝"nope"测试的是 Zod 而非 schema - 使用
z.input<typeof Schema>作为 fixture 类型,从观察到的负载构建
…
[为翻译而截断]