💎 Zod 4 现已稳定发布! 阅读公告。
Zod logo

发布说明

Edit this page

经过一年的积极开发:Zod 4 现已稳定发布!它更快、更轻、更符合 tsc 的优化,并且实现了一些长期请求的功能。

❤️

特别感谢 Clerk,通过他们极其慷慨的 开源基金会 支持了我对 Zod 4 的开发工作。在比预期更长的开发过程中,他们一直是很棒的合作伙伴。

版本管理

升级命令:

npm install zod@^4.0.0

关于所有重大变更的完整列表,请参阅 迁移指南。本文重点介绍新增功能和改进。

为什么发布新的主版本?

Zod v3.0 发布于 2021 年 5 月(!)。当时 Zod 在 GitHub 上只有 2700 颗星,并且每周下载量为 60 万。现在拥有 3.78 万星和 3100 万每周下载量(6 周前 beta 发布时是 2300 万!)。经过 24 个小版本之后,Zod 3 的代码库已经达到瓶颈;最常请求的功能和改进需要破坏兼容性的重大更改。

Zod 4 一举解决了 Zod 3 长期存在的设计限制,为多个长期开发的功能和性能飞跃铺平了道路。它关闭了 Zod 的 10 个最高票未解决 issue 中的 9 个。希望它能成为未来多年的新基础。

想快速浏览新增内容,请参阅目录,点击任意项目跳转相应章节。

性能基准测试

你可以在 Zod 仓库中自行运行以下基准测试:

$ git clone git@github.com:colinhacks/zod.git
$ cd zod
$ git switch v4
$ pnpm install

然后运行指定基准:

$ pnpm bench <name>

字符串解析快 14 倍

$ pnpm bench string
runtime: node v22.13.0 (arm64-darwin)
 
benchmark      time (avg)             (min max)       p75       p99      p999
------------------------------------------------- -----------------------------
 z.string().parse
------------------------------------------------- -----------------------------
zod3          363 µs/iter       (338 µs 683 µs)    351 µs    467 µs    572 µs
zod4       24'674 ns/iter    (21'083 ns 235 µs) 24'209 ns 76'125 ns    120 µs
 
summary for z.string().parse
  zod4
   14.71x faster than zod3

数组解析快 7 倍

$ pnpm bench array
runtime: node v22.13.0 (arm64-darwin)
 
benchmark      time (avg)             (min max)       p75       p99      p999
------------------------------------------------- -----------------------------
 z.array() parsing
------------------------------------------------- -----------------------------
zod3          147 µs/iter       (137 µs 767 µs)    140 µs    246 µs    520 µs
zod4       19'817 ns/iter    (18'125 ns 436 µs) 19'125 ns 44'500 ns    137 µs
 
summary for z.array() parsing
  zod4
   7.43x faster than zod3

对象解析快 6.5 倍

此测试运行的是 Moltar 验证库基准

$ pnpm bench object-moltar
benchmark      time (avg)             (min max)       p75       p99      p999
------------------------------------------------- -----------------------------
 z.object() safeParse
------------------------------------------------- -----------------------------
zod3          805 µs/iter     (771 µs 2'802 µs)    804 µs    928 µs  2'802 µs
zod4          124 µs/iter     (118 µs 1'236 µs)    119 µs    231 µs    329 µs
 
summary for z.object() safeParse
  zod4
   6.5x faster than zod3

tsc 实例数量降低 100 倍

考虑以下简单文件:

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/v3" 以及 tsc --extendedDiagnostics 编译该文件会产生超过 25000 次类型实例化。用 "zod/v4" 仅约 175 次。

Zod 仓库内有一个 tsc 基准测试游乐场。可以在 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 和代码库中表现更佳。

核心包体积减半

考虑以下极简脚本:

import * as z from "zod";
 
const schema = z.boolean();
 
schema.parse(true);

这是验证中尽可能简单的例子,有助于衡量核心包体积 —— 即即使在简单用例中仍包含的代码。我们用 rollup 对比 Zod 3 和 Zod 4 打包结果。

包名打包体积(gzip)
Zod 312.47kb
Zod 45.36kb

Zod 4 中核心包约减少 57%(2.3 倍)。这很好,但还有更大的提升空间。

介绍 Zod Mini

Zod 的方法繁重 API 本质上难以做树摇优化。即使是我们的简单 z.boolean() 脚本,也会带入许多未使用的实现,比如 .optional().array() 等。写更精简的实现只能到此为止。此时 Zod Mini 问世。

npm install zod@^4.0.0

它是符合 zod 功能一对一映射的变体,API 更函数式且支持树摇优化。Zod 采用链式方法,Zod Mini 通常用包装函数替代:

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() 方法用以添加细化规则。

import * as z from "zod/mini";
 
z.array(z.number()).check(
  z.minLength(5), 
  z.maxLength(10),
  z.refine(arr => arr.includes(5))
);

以下顶级细化在 Zod Mini 中可用。它们对应的 Zod 方法应该是相当自明的。

import * as z from "zod/mini";
 
// 自定义检查
z.refine();
 
// 一等公民检查
z.lt(value);
z.lte(value); // 别名: z.maximum()
z.gt(value);
z.gte(value); // 别名: 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); // 对象 schema,检查 `input[key]` 是否符合 `schema`
z.mime(value); // 文件 schema 相关 (见下文)
 
// 覆盖(*不会*改变推断类型!)
z.overwrite(value => newValue);
z.normalize();
z.trim();
z.toLowerCase();
z.toUpperCase();

此更函数式 API 让打包工具更容易摇掉未使用 API。推荐绝大多数场景使用 zod/v4,但对包体积限制苛刻的项目应考虑 zod/v4-mini

这个更实用的 API 使得打包工具更容易去除你不使用的 API。虽然常规的 Zod 仍然推荐用于大多数用例,但任何对包大小有不寻常严格限制的项目都应该考虑使用 Zod Mini。

核心包大小减少 6.6 倍

这是上面的脚本,已更新为使用 "zod/mini" 而不是 "zod"

import * as z from "zod/mini";
 
const schema = z.boolean();
schema.parse(false);

用 rollup 打包后的 gzip 体积是 1.88kb,相比 zod@3 核心包体积减少 85%(约 6.6 倍)。

包名打包体积(gzip)
Zod 312.47kb
Zod 4 (regular)5.36kb
Zod 4 (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: "..." })
// => 返回 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: "也允许额外属性"
});

.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");
 
// 等效于
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。

递归对象

这是一个意外收获。经过多年尝试解决这个问题,我终于找到了方法让 Zod 正确推断递归对象类型。定义递归类型:

const Category = z.object({
  name: z.string(),
  get subcategories(){
    return z.array(Category)
  }
});
 
type Category = z.infer<typeof Category>;
// { name: string; subcategories: Category[] }

还能表示互相递归类型

const User = z.object({
  email: z.email(),
  get posts(){
    return z.array(Post)
  }
});
 
const Post = z.object({
  title: z.string(),
  get author(){
    return User
  }
});

与 Zod 3 递归类型写法不同,无需类型断言。生成的 schema 是普通的 ZodObject 实例,支持完整方法集:

Post.pick({ title: true })
Post.partial();
Post.extend({ publishDate: z.date() });

文件类型模式

验证 File 实例:

const fileSchema = z.file();
 
fileSchema.min(10_000); // 最小 .size(字节)
fileSchema.max(1_000_000); // 最大 .size(字节)
fileSchema.mime(["image/png"]); // MIME 类型

国际化

Zod 4 引入全局翻译错误消息的 locales API。

import * as z from "zod";
 
// 配置英文(默认)
z.config(z.locales.en());

请参阅自定义错误中的完整支持语言列表;该部分会在有新支持语言时及时更新。

错误美化打印

zod-validation-error 的流行表明用户对官方美化错误的 API 需求很大。如果你已经用它,可继续使用。

Zod 新增了顶级函数 z.prettifyErrorZodError 转为用户友好的格式化字符串。

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]

当前格式不可配置,未来可能调整。

顶级字符串格式

所有“字符串格式”(邮箱等)均升级为 z 模块的顶级函数,更简洁且支持树摇。对应的方法式 API(如 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.lowercase();
z.iso.date();
z.iso.datetime();
z.iso.duration();
z.iso.time();

自定义邮箱正则

z.email() 支持自定义正则表达式。邮箱没有单一标准正则,不同应用可选择不同严格程度。为方便,Zod 导出常用正则:

// Zod 默认邮箱正则(Gmail 规则)
// 详见 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 });
 
// 经典邮箱正则(RFC 5322)
// 来源 emailregex.com
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}` (同时支持最小/最大长度细化!)

所有可转字符串的 Zod schema 类型(字符串、字符串格式如 z.email()、数字、布尔、大整数、枚举、字面量、undefined/optional、null/nullable 和其它模板字面量)都存有内部正则表达式。z.templateLiteral 将它们串联为超正则,可正确支持格式验证(自定义细化除外)。

详见 模板字面量文档

数字格式

已添加用于表示定宽整数和浮点类型的新数字“格式”。这些格式返回一个已添加适当包含性最小/最大约束的 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();    // Int64,范围 [-9223372036854775808n, 9223372036854775807n]
z.uint64();   // UInt64,范围 [0n, 18446744073709551615n]

字符串布尔(StringBool)

已有的 z.coerce.boolean() 非常简单:假值(falseundefinednull0""NaN 等)转换为 false,真值转换为 true

这仍是一个好用且与其它 z.coerce 接口一致的 API。但部分用户请求更复杂的“环境变量风格”布尔值转换。为此,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("enabled")      // => 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(/* 其他任何值 */); // 抛出 ZodError<[{ code: "invalid_value" }]>

可自定义真值和假值:

z.stringbool({
  truthy: ["yes", "true"],
  falsy: ["no", "false"]
})

详见 z.stringbool() 文档

简化的错误自定义

大多数破坏性更改涉及 错误自定义 API。Zod 3 时代它们比较混乱,Zod 4 使设计更优雅,值得着重说明。

简言之,现在统一使用单个 error 参数替代以下 API:

message 替换为 error。(message 依然支持但已弃用)

- z.string().min(5, { message: "Too short." });
+ z.string().min(5, { error: "Too short." });

invalid_type_errorrequired_error 替换为函数形式的 error

// Zod 3
- z.string({ 
-   required_error: "This field is required" 
-   invalid_type_error: "Not a string", 
- });
 
// Zod 4 
+ z.string({ error: (issue) => issue.input === undefined ? 
+  "This field is required" :
+  "Not a string" 
+ });

errorMap 替换为函数形式的 error

// Zod 3 
- z.string({
-   errorMap: (issue, ctx) => {
-     if (issue.code === "too_small") {
-       return { message: `Value must be >${issue.minimum}` };
-     }
-     return { message: ctx.defaultError };
-   },
- });
 
// Zod 4
+ z.string({
+   error: (issue) => {
+     if (issue.code === "too_small") {
+       return `Value must be >${issue.minimum}`
+     }
+   },
+ });

升级的 z.discriminatedUnion()

判别联合现在支持了先前不支持的多种 schema 类型,包括联合和管道(pipe):

const MyResult = z.discriminatedUnion("status", [
  // 简单字面量
  z.object({ status: z.literal("aaa"), data: z.string() }),
  // 判别属性使用联合类型
  z.object({ status: z.union([z.literal("bbb"), z.literal("ccc")]) }),
  // 判别属性使用管道转换
  z.object({ status: z.literal("fail").transform(val => val.toUpperCase()) }),
]);

更重要的是,判别联合现在支持复合 — 可以在一个判别联合成员里再使用判别联合。

const BaseError = z.object({ status: z.literal("failed"), message: z.string() });
 
const MyResult = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.discriminatedUnion("code", [
    BaseError.extend({ code: z.literal(400) }),
    BaseError.extend({ code: z.literal(401) }),
    BaseError.extend({ code: z.literal(500) })
  ])
]);

z.literal() 支持传入多个值

z.literal() 现在可选接受多个值。

const httpCodes = z.literal([ 200, 201, 202, 204, 206, 207, 208, 226 ]);

Zod 3 版本写法:

const httpCodes = z.union([
  z.literal(200),
  z.literal(201),
  z.literal(202),
  z.literal(204),
  z.literal(206),
  z.literal(207),
  z.literal(208),
  z.literal(226)
]);

细化仍存于 schemas 内部

Zod 3 时,细化存于一个包裹原有 schema 的 ZodEffects 类里,造成不能同时链式调用 .refine().min() 等方法:

z.string()
  .refine(val => val.includes("@"))
  .min(5);
// ^ ❌ Property 'min' does not exist on type ZodEffects<ZodString, string, string>

Zod 4 时,细化内嵌于 schema 本身,上面代码可正常工作:

z.string()
  .refine(val => val.includes("@"))
  .min(5); // ✅

.overwrite()

.transform() 极其有用,但有个缺点:输出类型无法在运行时“可 introspect”,转换函数是黑盒,可返回任意值。这意味着很多功能(例如 JSON Schema 转换)无法正确支持。

const Squared = z.number().transform(val => val ** 2);
// => ZodPipe<ZodNumber, ZodTransform>

Zod 4 新增 .overwrite() 表示“不会修改推断类型的转换”,返回原始类实例。覆盖函数存作细化,不改变推断类型。

z.number().overwrite(val => val ** 2).max(100);
// => ZodNumber

现有 .trim().toLowerCase().toUpperCase() 方法已改用 .overwrite() 实现。

可扩展基础:zod/v4/core

此功能对大多数用户无关,但值得强调。Zod Mini 的添加催生了共享子包 zod/v4/core,包含 Zod 和 Zod Mini 共享的核心功能。

起初我抗拒设立这个,但如今认为这是 Zod 4 最重要特性之一,使 Zod 从简单库变成可供其它库灵活集成的高性能验证“底座”。

如果你在构建 Schema 库,请参考 Zod 和 Zod Mini 的实现,了解如何基于 zod/v4/core 构建。欢迎通过 GitHub 讨论或 X/Bluesky 联系我以获取帮助或反馈。

总结

我计划发布多篇解释诸如 zod/v4-mini 之类重大功能设计思想的系列文章,届时会更新本节。

我计划写一系列额外的帖子,解释一些主要功能(如 Zod Mini)背后的设计过程。随着这些帖子发布,我会更新这一部分。

对于库的作者,现在有一个专门的 库作者指南,描述了在 Zod 上构建的最佳实践。它回答了关于如何同时支持 Zod 3 和 Zod 4(包括 Mini)的常见问题。

pnpm upgrade zod@latest

祝你解析愉快!
— Colin McDonnell @colinhacks