💎 Zod 4.6 已发布! 查看发布说明。

Zod 4.6

Colin McDonnell·

Zod 4.6 现已发布。

npm install zod@latest

概览:

.validate()

Zod、Zod Mini 和 Zod Core 均支持独立的布尔值验证。它无需构造 ZodError,即可判断“这个输入是否有效?”,因此拒绝无效输入的成本很低。返回类型是针对 schema 输入类型的类型守卫。

z.validate(z.string(), "hi"); // true
z.validate(z.string(), 42);   // false

Zod Classic schema 也提供此方法。(#6547)

const Player = z.object({
  username: z.string(),
  xp: z.number(),
});
 
if (Player.validate(data)) {
  data.username; // narrowed
}

结合 z.compile() 使用时,对无效输入的验证速度比 .safeParse().success 最多快 35 倍。

使用 z.compile() 编译的 schema 对无效输入每次调用所需时间,灰色柱为 safeParse().success,柱内蓝色部分为 .validate():3 个对象的 union 为 28 ns(快 34.9 倍),包含 10 个字符串的数组为 23 ns(快 24.6 倍),3 项 tuple 为 28 ns(快 16.1 倍),5 个键的对象为 21 ns(快 16.3 倍),包含 3 个选项的判别联合为 21 ns(快 15.7 倍),z.number() 为 19 ns(快 13.8 倍),z.string() 为 19 ns(快 13.2 倍),z.boolean() 为 19 ns(快 13.7 倍);最高快 34.9 倍
使用 z.compile() 编译的 schema 对无效输入的单次调用耗时;越低越好(基准测试)

两张图都测量失败路径。.validate() 的关键优势是遇到第一个 issue 时即可提前返回,不必汇总完整的 ZodIssue[] 数组。

注意 — 异步 refinement 可使用 .validateAsync()。

z.properties()

z.instanceof() 新增了 .properties() 方法,用于验证实例的指定属性。与 z.object() 不同,它会在原对象上进行验证,因此适用于 class 实例。(#6536)

const okResponse = z.instanceof(Response).properties({
  ok: z.literal(true),
  status: z.number().min(200).max(299),
});

输入会原样返回,因此原型得以保留,实例方法也能继续使用。这是 z.object() 做不到的:它会返回普通对象,原来的 Response 实例也就不复存在。

const res = await fetch("/api/user");
 
okResponse.parse(res) === res; // ✅ true

fromJSONSchema()

z.fromJSONSchema() 现在还支持六个 JSON Schema 关键字。(#6535)

const schema = z.fromJSONSchema({
  type: "object",
  minProperties: 2,      // also maxProperties
});
 
schema.parse({ a: 1 });        // ❌ too few properties
schema.parse({ a: 1, b: 2 });  // ✅

两个属性数量边界都会统计输入对象自身的键。数组唯一性按结构比较,因此 [{ a: 1 }, { a: 1 }] 会被视为重复项。

z.fromJSONSchema({ type: "array", uniqueItems: true }).parse([{ a: 1 }, { a: 1 }]); // ❌
 
z.fromJSONSchema({
  type: "array",
  contains: { type: "number" },   // also minContains and maxContains
  minContains: 2,
}).parse(["a", 2]);               // ❌ only one number

z.iban()

新增的字符串格式:电子格式的 IBAN,并通过有效的 ISO 7064 MOD 97-10 校验和验证。(#6571)

z.iban().parse("DE89370400440532013000"); // ✅
z.iban().parse("DE89370400440532013001"); // ❌ bad checksum

z.withParser()

z.compile() 使用 new Function 构建 parser,严格的 Content Security Policy 会阻止此操作。z.withParser() 单独提供了安装器:它接收由其他方式(例如构建时工具或原生编译器)生成的 parser,并按相同约定安装。(#6575)

const Player = z.object({ username: z.string(), xp: z.number() });
 
// isPlayer is a type guard your build step generated
const Fast = z.withParser(Player, (input) =>
  isPlayer(input) ? { username: input.username, xp: input.xp } : z.INVALID
);

所提供的 parser 负责生成完整结果,因此必须返回与 schema 相同的结果。此示例会重新构建对象,而不是直接返回输入,因为 z.object() 会移除未知键。返回 z.INVALID 则会把输入交给 runtime 处理;runtime 仍是生成 ZodError 的唯一来源。

塔吉克语语言包

塔吉克语(tg)在 4.6.1 中加入语言包列表。(#6581,贡献者 @ismoil77)

z.config(z.locales.tg());

更快的 CommonJS

TypeScript 会将重新导出编译为 getter;Zod 4.5 的 CommonJS 入口中,255 个导出有 252 个是 getter。V8 无法透过 getter 识别出固定的调用目标,因此不能内联调用。4.6 构建会生成普通属性,并冻结导出对象。对于已编译的 schema,在 require 下调用 z.validate() 比 Zod 4.5 快约 3 倍。(#6564)

const { z } = require("zod");
const CompiledPlayer = z.compile(Player);
 
z.validate(CompiledPlayer, data); // ~3x faster than Zod 4.5

只有通过命名空间调用的方式受到影响。Player.safeParse(data) 这样的实例方法调用不会读取导出对象,ESM 构建也没有变化。

更快地拒绝无效 z.url()

从 4.6.4 起,单独使用 z.url() 时会通过 URL.canParse() 验证,而不再调用会抛出异常的构造器,因此拒绝无效 URL 约需 90 ns,原先约需 4.6 µs。使用 hostname、protocol 或 normalize 选项时会调用 URL.parse();它会返回 null 而不是抛出异常,但处理有效的规范化 URL 会慢约 5%。(#6588)

z.validate(z.url(), "invalid"); // 4.6 µs in 4.6.3, 86 ns in 4.6.4

递归 schema 的内存保留

递归 schema 会一直持有上一次解析的输入和输出,直到下一次解析将其替换,因此一个长期存活的 schema 会固定住它处理过的所有对象。Zod 4.4 会释放该输入,4.5 则不会;这导致一次全仓库 lint 运行因内存耗尽而失败。现在解析状态中的引用都改为弱引用:解析一次包含 2.9 万个节点的树只保留 2.2 MB,之前为 10.1 MB;递归解析的耗时则增加约 6%。(#6572)

const Category = z.object({
  name: z.string(),
  get children() {
    return z.array(Category);
  },
});

错误修复

⚠️ 首次读取 error 时才会运行错误映射

由于 safeParse() 现在会延迟构造错误,全局、语言包以及 schema 上的 error 错误映射都会在首次读取 result.error 时运行,而不是在解析时运行。如果代码在解析后、读取错误前更换了 z.config(),则会使用较新的配置。(#6519)

const result = schema.safeParse(12);
z.config(z.locales.fr());
result.error.issues[0].message; // French in 4.6, English in 4.5

如果没有读取错误,带副作用的错误映射就不会运行。抛出式解析不受影响:.parse() 会立即构造并抛出错误,不会走延迟路径,堆栈仍会指向调用位置。

⚠️ z.emoji() 会拒绝只包含 emoji 组件的字符串

Unicode 的 Emoji_Component 属性涵盖了可附加到 emoji 上的组成部分,因此 z.emoji() 过去会接受 "123"、"#"、"*",以及单独出现的零宽连接符、变体选择符或肤色修饰符。现在的模式要求至少包含一个象形文字、区域指示符或键帽符号。(#6532)

z.emoji().parse("😀");   // ✅
z.emoji().parse("1️⃣");   // ✅ the keycap is the anchor
z.emoji().parse("123");  // ❌ was accepted in 4.5

旗帜、旗帜子区域、带肤色修饰符的 emoji 和 ZWJ 序列均不受影响。关闭 #6515。

⚠️ 数字 enum 的选项不再包含反向映射

数字类型的 TypeScript enum 在运行时还包含反向映射(例如 0 对应 "UK")。解析器此前会忽略这些键,但 .options 直接从 enum 对象读取,因此含三个成员的 enum 会列出六个值,其中三个无法通过解析。(#6542)

enum Country { UK, Germany, France }
 
z.enum(Country).options; // 4.5: ["UK", "Germany", "France", 0, 1, 2] — 4.6: [0, 1, 2]

⚠️ base64 模式

z.base64() 和 z.base64url() 的运行时模式现在只检查字符集合,长度和填充则由代码验证,因此多 MB 字符串通过组合 schema 解析时不再会导致正则表达式调用栈溢出。JSON Schema 输出仍会生成精确的分块模式,所以 z.toJSONSchema() 的结果不变。(#6534,#6527)

将 z.base64() 组合进模板字符串时,现在只会检查字符集合,不检查长度;z.creditCard() 在此处也采用相同规则。导出的 z.regexes.base64url 现在是带长度限制的模式,因此面对多 MB 输入时也会像 z.regexes.base64 一样溢出。

⚠️ email 模式移除了前瞻断言

z.email() 原先以两个前瞻断言开头,第二个会在匹配开始前扫描整个字符串。现在两者都已移除,它们原本负责的规则(local part 中不能有空片段)改为通过结构表达,因此 z.email() 接受和拒绝的输入与之前完全相同。有效地址的验证速度约快一倍。(#6573)

模式字符串是用户可见的,因此它的每个副本都会变化:现在 z.regexes.email 不含捕获组;它过去暴露的两个捕获组都没有可用值。失败的 z.email() 产生的 issue.pattern 以及 z.toJSONSchema() 输出的 pattern 也会变化。后者不再含前瞻断言,因此 ECMAScript 之外的验证器也能编译它。

将 email 组合进模板字符串时,也不再把“不能有连续句点”的规则应用到 email 片段以外的字符串。

z.templateLiteral([z.email(), "|", z.string()]).parse("[email protected]|a..b");
// 4.5: ❌ — the lookahead reached past the email segment — 4.6: ✅

⚠️ 链式检查不再互相覆盖 JSON Schema 中的约束

过去每添加一项检查,就会按链式调用顺序将其边界写入 schema,因此在 .min() 和 .max() 后添加格式检查时,较严格的值会被格式检查自身的范围覆盖。现在转换器会将这些检查折叠为合取条件,链式调用的顺序不再影响输出。(#6554,#6553)

z.toJSONSchema(z.number().min(0).max(23).int());
// 4.5: { minimum: -9007199254740991, maximum: 9007199254740991 }
// 4.6: { minimum: 0, maximum: 23 }

所有版本的运行时解析都会执行这些边界,错误只出在生成的 schema 上。同一折叠逻辑还修复了另外两种情况:重复调用 multipleOf 时只保留第一个除数、丢弃其余除数,因此 z.number().multipleOf(2).multipleOf(3) 生成的 schema 会接受 4;z.string().min(8).length(5) 则会生成 minLength: 5,放宽运行时仍会拒绝的边界。关闭 #6550。

⚠️ 元数据成员在首次读取时才会实例化

Zod Classic schema 的八个成员(.format、.minLength、.maxLength、.minValue、.maxValue、.isInt、.minDate 和 .maxDate)现在会根据各项检查计算,而不是在构造时写入每个实例。它们都是原型上的 getter,并会在首次读取时变为实例自身的属性。(#6554)

const s = z.string().min(3).max(9);
 
Object.keys(s); // 4.5: ["def", "type", "format", "minLength", "maxLength"] — 4.6: ["def", "type"]
s.minLength;    // 3 in both
Object.keys(s); // 4.6: ["def", "type", "minLength"]

在读取前,这些键不会出现在实例上;Object.assign({}, schema) 也只会复制已经读取过的成员。删除某个成员会恢复其 getter,下次读取时会重新计算。

这些值也可能变化,因为 getter 使用的折叠逻辑与 JSON Schema 转换器相同。现在,顺序相关的检查链会报告更严格的边界,而不是最后写入的那一项。

z.string().min(8).length(5).minLength; // 4.5: 5 — 4.6: 8

提交记录

Zod 4.6 汇集了 72 个提交。