Zod logoZod logo Zod logoZod logo 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()),
});

多年来,这一直让我很头疼。要定义一个循环对象类型,您必须

太糟糕了。

这是 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 值(falseundefinednull0""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 中有点混乱