Zod v4 Beta 版本发布
Search
⌘``K
Beta
Introducing Zod 4 betaMigration guide
Documentation
IntroBasic usageDefining schemasCustomizing errorsFormatting errorsMetadata and registriesNewJSON SchemaNewEcosystem
Packages
zod@zod/miniNew@zod/coreNew
github logo
On this page
Zod 4 Beta 版本发布
请参考 Changelog 获取完整的重大变更列表。
经过一年多的积极开发,Zod 4 现在进入 beta 测试阶段。它更快、更精简、tsc
效率更高,并实现了一些长期以来被要求的功能。
要安装 beta 版本:
pnpm upgrade zod@next
我将在 v4
分支上继续进行 4-6 周的 beta 开发,同时与各个库合作,以确保与第一个稳定版本实现首日兼容。
❤️ 非常感谢 Clerk,他们通过极其慷慨的 OSS Fellowship 资助了我对 Zod 4 的开发工作。在整个(比预期长得多!)开发过程中,他们都是一个了不起的合作伙伴。
为什么要发布新的主版本?
Zod v3.0 发布于 2021 年 5 月!当时 Zod 在 GitHub 上有 2700 个 star,每周下载量为 60 万次。如今,它拥有 36.5k 个 star 和 2300 万的每周下载量。经过 24 个次要版本,Zod 3 的代码库已经触及天花板;最常被要求的功能和改进都需要破坏性更改。
Zod 4 一举实现了所有这些。它使用了一个全新的内部架构,解决了长期存在的一些设计限制,为一些长期以来被要求的功能奠定了基础,并关闭了 Zod 10 个最受赞同的未解决 issue 中的 9 个。希望它能作为未来多年的新基础。
有关新增内容的简要概述,请参见目录。单击任何项目以跳转到该部分。
基准测试
您可以在 Zod 仓库中自行运行这些基准测试:
$ git clone git@github.com:colinhacks/zod.git
$ cd zod
$ git switch v4
$ pnpm install
然后运行特定的基准测试:
$ pnpm bench <name>
字符串解析速度提高 2.6 倍
$ pnpm bench string
runtime: node v22.13.0 (arm64-darwin)
benchmark time (avg) (min … max) p75 p99 p999
------------------------------------------------- -----------------------------
• z.string().parse
------------------------------------------------- -----------------------------
zod3 348 µs/iter (299 µs … 743 µs) 362 µs 494 µs 634 µs
zod4 132 µs/iter (108 µs … 348 µs) 162 µs 269 µs 322 µs
summary for z.string().parse
zod4
2.63x faster than zod3
数组解析速度提高 3 倍
$ pnpm bench array
runtime: node v22.13.0 (arm64-darwin)
benchmark time (avg) (min … max) p75 p99 p999
------------------------------------------------- -----------------------------
• z.array() parsing
------------------------------------------------- -----------------------------
zod3 162 µs/iter (141 µs … 753 µs) 152 µs 291 µs 513 µs
zod4 54'282 ns/iter (47'084 ns … 669 µs) 50'833 ns 185 µs 233 µs
summary for z.array() parsing
zod4
2.98x faster than zod3
对象解析速度提高 7 倍
这运行了 Moltar 验证库基准测试。
$ pnpm bench object-moltar
benchmark time (avg) (min … max) p75 p99 p999
------------------------------------------------- -----------------------------
• z.object() safeParse
------------------------------------------------- -----------------------------
zod3 767 µs/iter (735 µs … 3'136 µs) 775 µs 898 µs 3'136 µs
zod4 110 µs/iter (102 µs … 1'291 µs) 105 µs 217 µs 566 µs
summary for z.object() safeParse
zod4
6.98x faster than zod3
tsc
实例化减少 20 倍
考虑以下简单文件:
import * as z from "zod";
export const A = z.object({
a: z.string(),
b: z.string(),
c: z.string(),
d: z.string(),
e: z.string(),
});
export const B = A.extend({
f: z.string(),
g: z.string(),
h: z.string(),
});
使用 zod@3
,使用 tsc --extendedDiagnostics
编译此文件会导致 >25000 个类型实例化。使用 zod@4
,结果只有 ~1100 个。
Zod 仓库包含一个 tsc
基准测试 playground。使用 packages/tsc
中的编译器基准测试自行尝试。确切的数字可能会随着实现的演变而变化。
$ cd packages/tsc
$ pnpm bench object-with-extend
更重要的是,Zod 4 重新设计并简化了 ZodObject
和其他 schema 类的泛型,以避免一些有害的“实例化爆炸”。例如,重复链式调用 .extend()
和 .omit()
——这以前会导致编译器问题:
import * as z from "zod";
export const a = z.object({
a: z.string(),
b: z.string(),
c: z.string(),
});
export const b = a.omit({
a: true,
b: true,
c: true,
});
export const c = b.extend({
a: z.string(),
b: z.string(),
c: z.string(),
});
export const d = c.omit({
a: true,
b: true,
c: true,
});
export const e = d.extend({
a: z.string(),
b: z.string(),
c: z.string(),
});
export const f = e.omit({
a: true,
b: true,
c: true,
});
export const g = f.extend({
a: z.string(),
b: z.string(),
c: z.string(),
});
export const h = g.omit({
a: true,
b: true,
c: true,
});
export const i = h.extend({
a: z.string(),
b: z.string(),
c: z.string(),
});
export const j = i.omit({
a: true,
b: true,
c: true,
});
export const k = j.extend({
a: z.string(),
b: z.string(),
c: z.string(),
});
export const l = k.omit({
a: true,
b: true,
c: true,
});
export const m = l.extend({
a: z.string(),
b: z.string(),
c: z.string(),
});
export const n = m.omit({
a: true,
b: true,
c: true,
});
export const o = n.extend({
a: z.string(),
b: z.string(),
c: z.string(),
});
export const p = o.omit({
a: true,
b: true,
c: true,
});
export const q = p.extend({
a: z.string(),
b: z.string(),
c: z.string(),
});
在 Zod 3 中,这需要 4000ms
才能编译完成;并且添加额外的 .extend()
调用将触发“可能无限”错误。在 Zod 4 中,这在 400ms
内编译完成,速度提高了 10
倍。
结合即将推出的 tsgo
编译器,Zod 4 的编辑器性能将扩展到更大规模的 schema 和代码库。
核心 bundle 大小减少 2 倍
考虑以下简单脚本。
import * as z from "zod";
const schema = z.boolean();
schema.parse(true);
就验证而言,它尽可能简单。这是故意的;这是衡量 核心 bundle 大小 的好方法——即使在简单的情况下,最终也会出现在 bundle 中的代码。我们将使用 Zod 3 和 Zod 4 使用 rollup
打包它,并比较最终的 bundle。
Package| Bundle (gzip)
---|---
zod@3
| 12.47kb
zod@4
| 5.36kb
Zod 4 中的核心 bundle 大小缩小了约 57%(2.3 倍)。这很好!但我们可以做得更好。
Introducing @zod/mini
Zod 基于方法的 API 本质上难以进行 tree-shake。即使我们简单的 z.boolean()
脚本也会引入一堆我们没有使用的方法的实现,例如 .optional()
、.array()
等。编写更精简的实现只能让你走这么远。这就是 @zod/mini
的用武之地。
npm install @zod/mini@next
它是一个姊妹库,具有与 zod
一一对应的函数式、可 tree-shake 的 API。Zod 使用方法,而 @zod/mini
通常使用包装函数:
@zod/minizod
import * as z from "@zod/mini";
z.optional(z.string());
z.union([z.string(), z.number()]);
z.extend(z.object({ /* ... */ }), { age: z.number() });
并非所有方法都消失了!解析方法在 zod
和 @zod/mini
中是相同的。
import * as z from "@zod/mini";
z.string().parse("asdf");
z.string().safeParse("asdf");
await z.string().parseAsync("asdf");
await z.string().safeParseAsync("asdf");
还有一个通用的 .check()
方法,用于添加 refinements。
@zod/minizod
import * as z from "@zod/mini";
z.array(z.number()).check(
z.minLength(5),
z.maxLength(10),
z.refine(arr => arr.includes(5))
);
以下顶级 refinements 在 @zod/mini
中可用。它们与哪些 zod
方法对应关系应该相当不言自明。
import * as z from "@zod/mini";
// custom checks
z.refine();
// first-class checks
z.lt(value);
z.lte(value); // alias: z.maximum()
z.gt(value);
z.gte(value); // alias: z.minimum()
z.positive();
z.negative();
z.nonpositive();
z.nonnegative();
z.multipleOf(value);
z.maxSize(value);
z.minSize(value);
z.size(value);
z.maxLength(value);
z.minLength(value);
z.length(value);
z.regex(regex);
z.lowercase();
z.uppercase();
z.includes(value);
z.startsWith(value);
z.endsWith(value);
z.property(key, schema); // for object schemas; check `input[key]` against `schema`
z.mime(value); // for file schemas (see below)
// overwrites (these *do not* change the inferred type!)
z.overwrite(value => newValue);
z.normalize();
z.trim();
z.toLowerCase();
z.toUpperCase();
这种更具函数式的 API 使 bundler 更容易对您不使用的 API 进行 tree-shaking。虽然对于大多数用例仍然推荐使用 zod
,但任何具有非常严格的 bundle 大小限制的项目都应考虑使用 @zod/mini
。
核心 bundle 大小减少 6.6 倍
这是上面的脚本,更新为使用 "@zod/mini"
而不是 "zod"
。
import * as z from "@zod/mini";
const schema = z.boolean();
schema.parse(false);
当我们使用 rollup
构建它时,gzipped bundle 大小为 1.88kb
。与 zod@3
相比,核心 bundle 大小减少了 85%(6.6 倍)。
Package| Bundle (gzip)
---|---
zod@3
| 12.47kb
zod@4
| 5.36kb
@zod/mini
| 1.88kb
在专用的 @zod/mini
文档页面上了解更多信息。完整的 API 详细信息已混合到现有文档页面中;每当它们的 API 不同时,代码块都包含用于 zod
和 @zod/mini
的单独选项卡。
元数据
Zod 4 引入了一个新系统,用于向您的 schema 添加强类型元数据。元数据不存储在 schema 本身中;而是存储在“schema 注册表”中,该注册表将 schema 与某些类型化的元数据相关联。要使用 z.registry()
创建注册表:
import * as z from "zod";
const myRegistry = z.registry<{ title: string; description: string }>();
要将 schema 添加到您的注册表:
const emailSchema = z.string().email();
myRegistry.add(emailSchema, { title: "Email address", description: "..." });
myRegistry.get(emailSchema);
// => { title: "Email address", ... }
或者,为了方便起见,您可以使用 schema 上的 .register()
方法:
emailSchema.register(myRegistry, { title: "Email address", description: "..." })
// => returns emailSchema
全局注册表
Zod 还导出一个全局注册表 z.globalRegistry
,它接受一些常见的 JSON Schema 兼容元数据:
z.globalRegistry.add(z.string(), {
id: "email_address",
title: "Email address",
description: "Provide your email",
examples: ["naomie@example.com"],
extraKey: "Additional properties are also allowed"
});
.meta()
为了方便地将 schema 添加到 z.globalRegistry
,请使用 .meta()
方法。
z.string().meta({
id: "email_address",
title: "Email address",
description: "Provide your email",
examples: ["naomie@example.com"],
// ...
});
为了与 Zod 3 兼容,.describe()
仍然可用,但首选 .meta()
。
z.string().describe("An email address");
// equivalent to
z.string().meta({ description: "An email address" });
JSON Schema 转换
Zod 4 通过 z.toJSONSchema()
引入了第一方 JSON Schema 转换。
import * as z from "zod";
const mySchema = z.object({name: z.string(), points: z.number()});
z.toJSONSchema(mySchema);
// => {
// type: "object",
// properties: {
// name: {type: "string"},
// points: {type: "number"},
// },
// required: ["name", "points"],
// }
z.globalRegistry
中的任何元数据都会自动包含在 JSON Schema 输出中。
const mySchema = z.object({
firstName: z.string().describe("Your first name"),
lastName: z.string().meta({ title: "last_name" }),
age: z.number().meta({ examples: [12, 99] }),
});
z.toJSONSchema(mySchema);
// => {
// type: 'object',
// properties: {
// firstName: { type: 'string', description: 'Your first name' },
// lastName: { type: 'string', title: 'last_name' },
// age: { type: 'number', examples: [ 12, 99 ] }
// },
// required: [ 'firstName', 'lastName', 'age' ]
// }
有关自定义生成的 JSON Schema 的信息,请参阅 JSON Schema 文档。
z.interface()
Zod 4 引入了一个用于定义对象类型的新 API:z.interface()
。这可能看起来令人惊讶或困惑,所以我将在此处简要解释原因。(有关此主题的完整博客文章即将发布。)
更精确的可选属性
在 TypeScript 中,属性可以通过两种不同的方式“可选”:
type KeyOptional = { prop?: string };
type ValueOptional = { prop: string | undefined };
在 KeyOptional
中,可以从对象中省略 prop
键(“键可选”)。在 ValueOptional
中,必须设置 prop
键,但是可以将其设置为 undefined
(“值可选”)。
Zod 3 无法表示 ValueOptional
。相反,z.object()
会自动将问号添加到任何接受 undefined
值的键:
z.object({ name: z.string().optional() });
// { name?: string | undefined }
z.object({ name: z.union([z.string(), z.undefined()]) });
// { name?: string | undefined }
这包括特殊的 schema 类型,例如 z.unknown()
:
z.object({ name: z.unknown() }); // { name?: unknown }
z.object({ name: z.any() }); // { name?: any }
为了正确表示“键可选性”,Zod 需要一个用于将键标记为可选的 对象级别 API,而不是尝试根据值 schema 进行猜测。
这就是为什么 Zod 4 引入了一个用于定义对象类型的新 API:z.interface()
。
const ValueOptional = z.interface({ name: z.string().optional()});
// { name: string | undefined }
const KeyOptional = z.interface({ "name?": z.string() });
// { name?: string }
现在使用键本身的 ?
后缀定义键可选性。这样,您就可以区分键可选性和值可选性。
除了可选性的这种变化之外,z.object()
和 z.interface()
在功能上是相同的。它们甚至在内部使用相同的解析器。
z.object()
API 未弃用;如果您愿意,可以继续使用它!为了向后兼容性,添加了 z.interface()
作为选择加入的 API。
真正的递归类型
但是等等,还有更多!在实现 z.interface()
之后,我有一个巨大的认识。z.interface()
中的 ?
-后缀 API 使 Zod 可以绕过一个 TypeScript 限制,该限制长期以来一直阻止 Zod 干净地表示递归(循环)类型。以下是旧 Zod 3 文档中的示例:
import * as z from "zod"; // zod@3
interface Category {
name: string;
subcategories: Category[];
};
const Category: z.ZodType<Category> = z.object({
name: z.string(),
subcategories: z.lazy(() => Category.array()),
});
多年来,这一直让我很头疼。要定义一个循环对象类型,您必须
- 定义一个冗余接口
- 使用
z.lazy()
避免引用错误 - 将您的 schema 转换为
z.ZodType
太糟糕了。
这是 Zod 4 中的相同示例:
import * as z from "zod"; // zod@4
const Category = z.interface({
name: z.string(),
get subcategories() {
return z.array(Category)
}
});
无需类型转换,无需 z.lazy()
,也无需冗余类型签名。只需使用 getters 定义任何循环属性。生成的实例具有您期望的所有对象方法:
Category.pick({ subcategories: true });
这意味着 Zod 终于可以表示常见的循环数据结构,例如 ORM schema、GraphQL 类型等。
鉴于其表示循环类型和更精确的可选性的能力,我建议始终无保留地使用 z.interface()
而不是 z.object()
。也就是说,z.object()
永远不会被弃用或删除,因此如果您愿意,可以继续使用它。
文件 schemas
要验证 File
实例:
const fileSchema = z.file();
fileSchema.min(10_000); // minimum .size (bytes)
fileSchema.max(1_000_000); // maximum .size (bytes)
fileSchema.type("image/png"); // MIME type
国际化
Zod 4 引入了一个新的 locales
API,用于将错误消息全局翻译成不同的语言。
import * as z from "zod";
// configure English locale (default)
z.config(z.core.locales.en());
在撰写本文时,只有英语 locale 可用;很快就会向社区征求 pull request;当它们可用时,本节将更新为支持的语言列表。
错误美化打印
zod-validation-error
包的成功表明,对官方 API 进行美化打印错误存在大量需求。如果您目前正在使用该包,请务必继续使用它。
Zod 现在实现了一个顶级 z.prettifyError
函数,用于将 ZodError
转换为用户友好的格式化字符串。
const myError = new z.ZodError([
{
code: 'unrecognized_keys',
keys: [ 'extraField' ],
path: [],
message: 'Unrecognized key: "extraField"'
},
{
expected: 'string',
code: 'invalid_type',
path: [ 'username' ],
message: 'Invalid input: expected string, received number'
},
{
origin: 'number',
code: 'too_small',
minimum: 0,
inclusive: true,
path: [ 'favoriteNumbers', 1 ],
message: 'Too small: expected number to be >=0'
}
]);
z.prettifyError(myError);
这将返回以下可美化打印的多行字符串:
✖ Unrecognized key: "extraField"
✖ Invalid input: expected string, received number
→ at username
✖ Invalid input: expected number, received string
→ at favoriteNumbers[1]
目前,格式是不可配置的;将来可能会改变。
顶级字符串格式
所有“字符串格式”(email 等)都已提升为 z
模块上的顶级函数。这既更简洁,也更易于 tree-shaking。方法等效项(z.string().email()
等)仍然可用,但已被弃用。它们将在下一个主版本中删除。
z.email();
z.uuidv4();
z.uuidv7();
z.uuidv8();
z.ipv4();
z.ipv6();
z.cidrv4();
z.cidrv6();
z.url();
z.e164();
z.base64();
z.base64url();
z.jwt();
z.ascii();
z.utf8();
z.lowercase();
z.iso.date();
z.iso.datetime();
z.iso.duration();
z.iso.time();
自定义 email 正则表达式
z.email()
API 现在支持自定义正则表达式。没有一个规范的 email 正则表达式;不同的应用程序可以选择更严格或更宽松。为方便起见,Zod 导出了一些常见的表达式。
// Zod 的默认 email 正则表达式(Gmail 规则)
// see colinhacks.com/essays/reasonable-email-regex
z.email(); // z.regexes.email
// 浏览器用于验证 input[type=email] 字段的正则表达式
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email
z.email({ pattern: z.regexes.html5Email });
// 经典的 emailregex.com 正则表达式 (RFC 5322)
z.email({ pattern: z.regexes.rfc5322Email });
// 一个允许 Unicode 的宽松正则表达式(适用于国际电子邮件)
z.email({ pattern: z.regexes.unicodeEmail });
模板字面量类型
Zod 4 实现了 z.templateLiteral()
。模板字面量类型可能是 TypeScript 类型系统中以前无法表示的最大特性。
const hello = z.templateLiteral(["hello, ", z.string()]);
// `hello, ${string}`
const cssUnits = z.enum(["px", "em", "rem", "%"]);
const css = z.templateLiteral([z.number(), cssUnits]);
// `${number}px` | `${number}em` | `${number}rem` | `${number}%`
const email = z.templateLiteral([
z.string().min(1),
"@",
z.string().max(64),
]);
// `${string}@${string}` (the min/max refinements are enforced!)
可以字符串化的每个 Zod schema 类型都存储一个内部正则表达式:字符串、字符串格式(如 z.email()
)、数字、布尔值、bigint、枚举、字面量、undefined/optional、null/nullable 和其他模板字面量。z.templateLiteral
构造函数将它们连接成一个超级正则表达式,因此可以正确强制执行字符串格式(z.email()
)等(但自定义 refinements 不会!)。
阅读 模板字面量文档 以获取更多信息。
数字格式
添加了新的数字“格式”,用于表示固定宽度的整数和浮点类型。这些返回一个 ZodNumber
实例,其中已添加了适当的最小值/最大值约束。
z.int(); // [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
z.float32(); // [-3.4028234663852886e38, 3.4028234663852886e38]
z.float64(); // [-1.7976931348623157e308, 1.7976931348623157e308]
z.int32(); // [-2147483648, 2147483647]
z.uint32(); // [0, 4294967295]
类似地,还添加了以下 bigint
数字格式。这些整数类型超出了 JavaScript 中 number
可以安全表示的范围,因此这些返回一个 ZodBigInt
实例,其中已添加了适当的最小值/最大值约束。
z.int64(); // [-9223372036854775808n, 9223372036854775807n]
z.uint64(); // [0n, 18446744073709551615n]
Stringbool
现有的 z.coerce.boolean()
API 非常简单:falsy 值(false
、undefined
、null
、0
、""
、NaN
等)变为 false
,truthy 值变为 true
。
这仍然是一个很好的 API,并且其行为与其他 z.coerce
API 一致。但是一些用户请求了更复杂的“env 风格”布尔值强制转换。为了支持这一点,Zod 4 引入了 z.stringbool()
:
const strbool = z.stringbool();
strbool.parse("true") // => true
strbool.parse("1") // => true
strbool.parse("yes") // => true
strbool.parse("on") // => true
strbool.parse("y") // => true
strbool.parse("enable") // => true
strbool.parse("false"); // => false
strbool.parse("0"); // => false
strbool.parse("no"); // => false
strbool.parse("off"); // => false
strbool.parse("n"); // => false
strbool.parse("disabled"); // => false
strbool.parse(/* anything else */); // ZodError<[{ code: "invalid_value" }]>
要自定义 truthy 和 falsy 值:
z.stringbool({
truthy: ["yes", "true"],
falsy: ["no", "false"]
})
有关更多信息,请参阅 z.stringbool()
文档。
简化的错误自定义
Zod 4 中的大多数破坏性更改都涉及 错误自定义 API。它们在 Zod 3 中有点混乱