Zod logo

发行说明

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

🌐 After a year of active development: Zod 4 is now stable! It's faster, slimmer, more tsc-efficient, and implements some long-requested features.

❤️

衷心感谢Clerk,他们通过非常慷慨的开源软件奖学金支持了我在 Zod 4 上的工作。在整个(比预期长得多的!)开发过程中,他们一直是一个了不起的合作伙伴。

版本控制

🌐 Versioning

要升级:

🌐 To upgrade:

npm install zod@^4.0.0

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

🌐 For a complete list of breaking changes, refer to the Migration guide. This post focuses on new features & enhancements.

为什么要推出新的主要版本?

🌐 Why a new major version?

Zod v3.0 于 2021 年 5 月发布(!)。那时 Zod 在 GitHub 上有 2700 个星标,每周下载量为 60 万。现在它有 3.78 万个星标,每周下载量为 3100 万(相比 6 周前测试版发布时的 2300 万有所增加!)。经过 24 个小版本,Zod 3 的代码库已经达到瓶颈;最常被请求的功能和改进需要破坏性更改。

🌐 Zod v3.0 was released in May 2021 (!). Back then Zod had 2700 stars on GitHub and 600k weekly downloads. Today it has 37.8k stars and 31M weekly downloads (up from 23M when the beta came out 6 weeks ago!). After 24 minor versions, the Zod 3 codebase had hit a ceiling; the most commonly requested features and improvements require breaking changes.

Zod 4 一举修复了 Zod 3 的许多长期存在的设计限制,为多个长期请求的功能以及性能的大幅提升铺平了道路。它解决了 Zod 的 10 个最受欢迎的未解决问题 中的 9 个。希望它将成为未来多年新的基础。

🌐 Zod 4 fixes a number of long-standing design limitations of Zod 3 in one fell swoop, paving the way for several long-requested features and a huge leap in performance. It closes 9 of Zod's 10 most upvoted open issues. With luck, it will serve as the new foundation for many more years to come.

如需快速了解新增内容,请参阅目录。点击任意项目即可跳转到该部分。

🌐 For a scannable breakdown of what's new, see the table of contents. Click on any item to jump to that section.

基准测试

🌐 Benchmarks

你可以在 Zod 代码库中自行运行这些基准测试:

🌐 You can run these benchmarks yourself in the Zod repo:

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

然后运行特定的基准测试:

🌐 Then to run a particular benchmark:

$ pnpm bench <name>

字符串解析速度提高 14 倍

🌐 14x faster string parsing

$ 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 倍

🌐 7x faster array parsing

$ 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 倍

🌐 6.5x faster object parsing

这运行了 Moltar 验证库基准

🌐 This runs the Moltar validation library benchmark.

$ 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 倍

🌐 100x reduction in tsc instantiations

考虑以下简单文件:

🌐 Consider the following simple file:

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 个。

🌐 Compiling this file with tsc --extendedDiagnostics using "zod/v3" results in >25000 type instantiations. With "zod/v4" it only results in ~175.

Zod 仓库包含一个 tsc 基准测试 playground。使用 packages/tsc 中的编译器基准测试自己尝试一下。随着实现的发展,具体数字可能会变化。

🌐 The Zod repo contains a tsc benchmarking playground. Try this for yourself using the compiler benchmarks in packages/tsc. The exact numbers may change as the implementation evolves.

$ cd packages/tsc
$ pnpm bench object-with-extend

更重要的是,Zod 4 已经重新设计并简化了 ZodObject 和其他 schema 类的泛型,以避免一些有害的“实例化爆炸”。例如,重复链式调用 .extend().omit() —— 之前会导致编译器问题的情况:

🌐 More importantly, Zod 4 has redesigned and simplified the generics of ZodObject and other schema classes to avoid some pernicious "instantiation explosions". For instance, chaining .extend() and .omit() repeatedly—something that previously caused compiler issues:

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 中,这在 400ms10x 中更快地编译。

🌐 In Zod 3, this took 4000ms to compile; and adding additional calls to .extend() would trigger a "Possibly infinite" error. In Zod 4, this compiles in 400ms, 10x faster.

结合即将推出的 tsgo 编译器,Zod 4 的编辑器性能将扩展到更大规模的模式和代码库。

核心包大小减少 2 倍

🌐 2x reduction in core bundle size

考虑以下简单脚本。

🌐 Consider the following simple script.

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

在验证方面,它就是尽可能简单。这是有意为之;这是衡量核心包大小的好方法——即使在简单情况下最终也会包含在包中的代码。我们将使用 Zod 3 和 Zod 4 将其与 rollup 一起打包,并比较最终的包大小。

🌐 It's about as simple as it gets when it comes to validation. That's intentional; it's a good way to measure the core bundle size—the code that will end up in the bundle even in simple cases. We'll bundle this with rollup using both Zod 3 and Zod 4 and compare the final bundles.

软件包打包包 (gzip)
Zod 312.47kb
Zod 45.36kb

在 Zod 4 中,核心打包包大约小了 57%(2.3 倍)。这很好!但我们可以做得更好。

🌐 The core bundle is ~57% smaller in Zod 4 (2.3x). That's good! But we can do a lot better.

Zod Mini 简介

🌐 Introducing Zod Mini

Zod 的方法密集型 API 从根本上难以进行 tree-shake。即使是我们简单的 z.boolean() 脚本,也会引入很多我们没有使用的方法的实现,比如 .optional().array() 等。编写更精简的实现只能达到有限的效果。这就是 Zod Mini 的用武之地。

🌐 Zod's method-heavy API is fundamentally difficult to tree-shake. Even our simple z.boolean() script pulls in the implementations of a bunch of methods we didn't use, like .optional(), .array(), etc. Writing slimmer implementations can only get you so far. That's where Zod Mini comes in.

npm install zod@^4.0.0

这是一个 Zod 变体,具有功能性、可树摇优化的 API,与 zod 一一对应。Zod 使用方法,而 Zod Mini 通常使用封装函数:

🌐 It's a Zod variant with a functional, tree-shakable API that corresponds one-to-one with zod. Where Zod uses methods, Zod Mini generally uses wrapper functions:

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 中的解析方法是相同的:

🌐 Not all methods are gone! The parsing methods are identical in Zod and 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() 方法用于添加改进。

🌐 There's also a general-purpose .check() method used to add refinements.

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 方法应该比较容易理解。

🌐 The following top-level refinements are available in Zod Mini. It should be fairly self-explanatory which Zod methods they correspond to.

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 使打包工具更容易进行树摇,去掉你不使用的 API。虽然在大多数使用场景下仍然推荐使用常规的 Zod,但对于有极其严格包大小限制的项目,应考虑使用 Zod Mini。

🌐 This more functional API makes it easier for bundlers to tree-shake the APIs you don't use. While regular Zod is still recommended for the majority of use cases, any projects with uncommonly strict bundle size constraints should consider Zod Mini.

核心包大小减少 6.6 倍

🌐 6.6x reduction in core bundle size

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

🌐 Here's the script from above, updated to use "zod/mini" instead of "zod".

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

当我们使用 rollup 构建这个时,gzipped 包的大小是 1.88kb。与 zod@3 相比,这在核心包大小上减少了 85%(6.6 倍)。

🌐 When we build this with rollup, the gzipped bundle size is 1.88kb. That's an 85% (6.6x) reduction in core bundle size compared to zod@3.

包(gzip)
Zod 312.47kb
Zod 4(常规)5.36kb
Zod 4(迷你)1.88kb

请在专门的 zod/mini 文档页面上了解更多信息。完整的 API 详情混合在现有的文档页面中;代码块在其 API 不同之处包含 "Zod""Zod Mini" 的独立标签。

🌐 Learn more on the dedicated zod/mini docs page. Complete API details are mixed into existing documentation pages; code blocks contain separate tabs for "Zod" and "Zod Mini" wherever their APIs diverge.

元数据

🌐 Metadata

Zod 4 引入了一个用于向你的模式添加强类型元数据的新系统。元数据不会存储在模式本身内;相反,它存储在一个“模式注册表”中,该注册表将某个模式与一些类型化的元数据关联起来。要使用 z.registry() 创建注册表:

🌐 Zod 4 introduces a new system for adding strongly-typed metadata to your schemas. Metadata isn't stored inside the schema itself; instead it's stored in a "schema registry" that associates a schema with some typed metadata. To create a registry with z.registry():

import * as z from "zod";
 
const myRegistry = z.registry<{ title: string; description: string }>();

要将架构添加到注册表:

🌐 To add schemas to your registry:

const emailSchema = z.string().email();
 
myRegistry.add(emailSchema, { title: "Email address", description: "..." });
myRegistry.get(emailSchema);
// => { title: "Email address", ... }

或者,你可以出于方便在一个模式上使用 .register() 方法:

🌐 Alternatively, you can use the .register() method on a schema for convenience:

emailSchema.register(myRegistry, { title: "Email address", description: "..." })
// => returns emailSchema

全局注册表

🌐 The global registry

Zod 还导出一个全局注册表 z.globalRegistry,它接受一些常见的兼容 JSON Schema 的元数据:

🌐 Zod also exports a global registry z.globalRegistry that accepts some common JSON Schema-compatible metadata:

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()

要方便地将模式添加到 z.globalRegistry,请使用 .meta() 方法。

🌐 To conveniently add a schema to z.globalRegistry, use the .meta() method.

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 转换

🌐 JSON Schema conversion

Zod 4 通过 z.toJSONSchema() 引入了第一方 JSON Schema 转换。

🌐 Zod 4 introduces first-party JSON Schema conversion via z.toJSONSchema().

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 模式输出中。

🌐 Any metadata in z.globalRegistry is automatically included in the JSON Schema output.

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 模式的信息,请参阅 JSON Schema 文档

🌐 Refer to the JSON Schema docs for information on customizing the generated JSON Schema.

递归对象

🌐 Recursive objects

这是一个意外的发现。在多年尝试解决这个问题后,我终于找到了一种方法来在 Zod 中正确推断递归对象类型。要定义一个递归类型:

🌐 This was an unexpected one. After years of trying to crack this problem, I finally found a way to properly infer recursive object types in Zod. To define a recursive type:

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

你也可以表示相互递归类型

🌐 You can also represent mutually recursive types:

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 模式不同,不需要类型转换。生成的模式是普通的 ZodObject 实例,并且可以使用完整的方法集。

🌐 Unlike the Zod 3 pattern for recursive types, there's no type casting required. The resulting schemas are plain ZodObject instances and have the full set of methods available.

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

文件模式

🌐 File schemas

验证 File 实例:

🌐 To validate File instances:

const fileSchema = z.file();
 
fileSchema.min(10_000); // minimum .size (bytes)
fileSchema.max(1_000_000); // maximum .size (bytes)
fileSchema.mime(["image/png"]); // MIME type

国际化

🌐 Internationalization

Zod 4 引入了一个新的 locales API,用于将错误消息全局翻译成不同的语言。

🌐 Zod 4 introduces a new locales API for globally translating error messages into different languages.

import * as z from "zod";
 
// configure English locale (default)
z.config(z.locales.en());

自定义错误 中查看支持的语言环境的完整列表;此部分会随着新支持语言的推出而不断更新。

🌐 See the full list of supported locales in Customizing errors; this section is always updated with a list of supported languages as they become available.

错误美观打印

🌐 Error pretty-printing

zod-validation-error 包的流行表明,对于用于美化错误输出的官方 API 存在显著的需求。如果你当前正在使用该包,请务必继续使用它。

🌐 The popularity of the zod-validation-error package demonstrates that there's significant demand for an official API for pretty-printing errors. If you are using that package currently, by all means continue using it.

Zod 现在实现了一个顶层的 z.prettifyError 函数,用于将 ZodError 转换为用户友好的格式化字符串。

🌐 Zod now implements a top-level z.prettifyError function for converting a ZodError to a user-friendly formatted string.

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);

这将返回以下可漂亮打印的多行字符串:

🌐 This returns the following pretty-printable multi-line string:

✖ Unrecognized key: "extraField"
✖ Invalid input: expected string, received number
  → at username
✖ Invalid input: expected number, received string
  → at favoriteNumbers[1]

目前格式不可配置;将来可能会有所变化。

🌐 Currently the formatting isn't configurable; this may change in the future.

顶层字符串格式

🌐 Top-level string formats

所有“字符串格式”(电子邮件等)已被提升为 z 模块的顶层函数。这既更加简洁,也更易于进行树摇优化。方法等效项(z.string().email() 等)仍然可用,但已被弃用。它们将在下一个主要版本中被移除。

🌐 All "string formats" (email, etc.) have been promoted to top-level functions on the z module. This is both more concise and more tree-shakable. The method equivalents (z.string().email(), etc.) are still available but have been deprecated. They'll be removed in the next major version.

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();

自定义电子邮件正则表达式

🌐 Custom email regex

z.email() API 现在支持自定义正则表达式。没有一个通用的电子邮件正则表达式;不同的应用可能会选择更严格或更宽松的规则。为了方便,Zod 导出了一些常用的正则表达式。

🌐 The z.email() API now supports a custom regular expression. There is no one canonical email regex; different applications may choose to be more or less strict. For convenience Zod exports some common ones.

// Zod's default email regex (Gmail rules)
// see colinhacks.com/essays/reasonable-email-regex
z.email(); // z.regexes.email
 
// the regex used by browsers to validate input[type=email] fields
// https://web.nodejs.cn/en-US/docs/Web/HTML/Element/input/email
z.email({ pattern: z.regexes.html5Email });
 
// the classic emailregex.com regex (RFC 5322)
z.email({ pattern: z.regexes.rfc5322Email });
 
// a loose regex that allows Unicode (good for intl emails)
z.email({ pattern: z.regexes.unicodeEmail });

模板字面量类型

🌐 Template literal types

Zod 4 实现了 z.templateLiteral()。模板字面量类型可能是 TypeScript 类型系统中之前无法表示的最大特性。

🌐 Zod 4 implements z.templateLiteral(). Template literal types are perhaps the biggest feature of TypeScript's type system that wasn't previously representable.

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 模式类型都会存储一个内部正则表达式:字符串、像 z.email() 这样的字符串格式、数字、布尔值、大整数、枚举、字面量、未定义/可选、null/可空,以及其他模板字面量。z.templateLiteral 构造函数将这些拼接成一个超级正则表达式,所以字符串格式(z.email())等内容可以被正确强制执行(但自定义精炼则不行!)。

🌐 Every Zod schema type that can be stringified stores an internal regex: strings, string formats like z.email(), numbers, boolean, bigint, enums, literals, undefined/optional, null/nullable, and other template literals. The z.templateLiteral constructor concatenates these into a super-regex, so things like string formats (z.email()) are properly enforced (but custom refinements are not!).

阅读 模板字面量文档 以获取更多信息。

🌐 Read the template literal docs for more info.

数字格式

🌐 Number formats

已添加新的数字“格式”来表示固定宽度的整数和浮点类型。这些将返回一个带有已添加适当包含的最小/最大约束的 ZodNumber 实例。

🌐 New numeric "formats" have been added for representing fixed-width integer and float types. These return a ZodNumber instance with proper inclusive minimum/maximum constraints already added.

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 实例,并且已经添加了适当的最小/最大包含约束。

🌐 Similarly the following bigint numeric formats have also been added. These integer types exceed what can be safely represented by a number in JavaScript, so these return a ZodBigInt instance with the proper inclusive minimum/maximum constraints already added.

z.int64();    // [-9223372036854775808n, 9223372036854775807n]
z.uint64();   // [0n, 18446744073709551615n]

Stringbool

现有的 z.coerce.boolean() API 非常简单:假值(falseundefinednull0""NaN 等)会变成 false,真值会变成 true

🌐 The existing z.coerce.boolean() API is very simple: falsy values (false, undefined, null, 0, "", NaN etc) become false, truthy values become true.

这仍然是一个很好的 API,它的行为与其他 z.coerce API 保持一致。但一些用户请求一种更复杂的“环境风格”布尔强制转换。为支持这一点,Zod 4 引入了 z.stringbool()

🌐 This is still a good API, and its behavior aligns with the other z.coerce APIs. But some users requested a more sophisticated "env-style" boolean coercion. To support this, Zod 4 introduces 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(/* anything else */); // ZodError<[{ code: "invalid_value" }]>

自定义真值和假值:

🌐 To customize the truthy and falsy values:

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

有关更多信息,请参阅 z.stringbool() 文档

🌐 Refer to the z.stringbool() docs for more information.

简化错误定制

🌐 Simplified error customization

Zod 4 中的大多数破坏性更改涉及 错误自定义 API。在 Zod 3 中它们有点混乱;Zod 4 使事情变得更加优雅,以至于我认为值得在这里重点说明。

🌐 The majority of breaking changes in Zod 4 involve the error customization APIs. They were a bit of a mess in Zod 3; Zod 4 makes things significantly more elegant, to the point where I think it's worth highlighting here.

长话短说,现在有一个单一的、统一的 error 参数用于自定义错误,取代了以下 API:

🌐 Long story short, there is now a single, unified error parameter for customizing errors, replacing the following APIs:

message 替换为 error。(message 参数仍然支持,但已弃用。)

🌐 Replace message with error. (The message parameter is still supported but deprecated.)

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

invalid_type_errorrequired_error 替换为 error(函数语法):

🌐 Replace invalid_type_error and required_error with error (function syntax):

// 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(函数语法):

🌐 Replace errorMap with error (function syntax):

// 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()

🌐 Upgraded z.discriminatedUnion()

可区分联合现在支持许多以前不支持的模式类型,包括联合和管道:

🌐 Discriminated unions now support a number of schema types not previously supported, including unions and pipes:

const MyResult = z.discriminatedUnion("status", [
  // simple literal
  z.object({ status: z.literal("aaa"), data: z.string() }),
  // union discriminator
  z.object({ status: z.union([z.literal("bbb"), z.literal("ccc")]) }),
  // pipe discriminator
  z.object({ status: z.literal("fail").transform(val => val.toUpperCase()) }),
]);

也许最重要的是,判别联合现在可以组合——你可以将一个判别联合作为另一个的成员使用。

🌐 Perhaps most importantly, discriminated unions now compose—you can use one discriminated union as a member of another.

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() 中的多个值

🌐 Multiple values in z.literal()

z.literal() API 现在可选择支持多个值。

🌐 The z.literal() API now optionally supports multiple values.

const httpCodes = z.literal([ 200, 201, 202, 204, 206, 207, 208, 226 ]);
 
// previously in 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)
]);

细化存在于模式中

🌐 Refinements live inside schemas

在 Zod 3 中,它们存储在一个封装了原始模式的 ZodEffects 类中。这很不方便,因为这意味着你无法将 .refine() 与其他模式方法如 .min() 交错使用。

🌐 In Zod 3, they were stored in a ZodEffects class that wrapped the original schema. This was inconvenient, as it meant you couldn't interleave .refine() with other schema methods like .min().

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

在 Zod 4 中,细化存储在模式本身内部,因此上述代码可以正常工作。

🌐 In Zod 4, refinements are stored inside the schemas themselves, so the code above works as expected.

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

.overwrite()

.transform() 方法非常有用,但它有一个主要缺点:输出类型在运行时不再可自省。转换函数是一个黑箱,可以返回任何内容。这意味着(除其他外)没有可靠的方法将模式转换为 JSON Schema。

🌐 The .transform() method is extremely useful, but it has one major downside: the output type is no longer introspectable at runtime. The transform function is a black box that can return anything. This means (among other things) there's no sound way to convert the schema to JSON Schema.

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

Zod 4 引入了一种新的 .overwrite() 方法,用于表示 不会改变推断类型 的转换。与 .transform() 不同,这种方法返回原始类的实例。覆盖函数作为细化存储,因此它不会(也不能)修改推断类型。

🌐 Zod 4 introduces a new .overwrite() method for representing transforms that don't change the inferred type. Unlike .transform(), this method returns an instance of the original class. The overwrite function is stored as a refinement, so it doesn't (and can't) modify the inferred type.

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

现有的 .trim().toLowerCase().toUpperCase() 方法已使用 .overwrite() 重新实现。

一个可扩展的基础:zod/v4/core

🌐 An extensible foundation: zod/v4/core

虽然这对大多数 Zod 用户来说并不相关,但值得强调。Zod Mini 的加入需要创建一个共享的子包 zod/v4/core,其中包含 Zod 和 Zod Mini 之间共享的核心功能。

🌐 While this will not be relevant to the majority of Zod users, it's worth highlighting. The addition of Zod Mini necessitated the creation of a shared sub-package zod/v4/core which contains the core functionality shared between Zod and Zod Mini.

起初我对此很抗拒,但现在我认为这是 Zod 4 最重要的特性之一。它让 Zod 从一个简单的库升级为一个快速验证的“底层”,可以撒入到其他库中使用。

🌐 I was resistant to this at first, but now I see it as one of Zod 4's most important features. It lets Zod level up from a simple library to a fast validation "substrate" that can be sprinkled into other libraries.

如果你正在构建一个模式库,可以参考 Zod 和 Zod Mini 的实现,看看如何在 zod/v4/core 提供的基础上进行构建。遇到问题或需要反馈,不要犹豫,通过 GitHub 讨论或通过 X/Bluesky 联系我们获取帮助。

🌐 If you're building a schema library, refer to the implementations of Zod and Zod Mini to see how to build on top of the foundation zod/v4/core provides. Don't hesitate to get in touch in GitHub discussions or via X/Bluesky for help or feedback.

总结

🌐 Wrapping up

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

🌐 I'm planning to write up a series of additional posts explaining the design process behind some major features like Zod Mini. I'll update this section as those get posted.

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

🌐 For library authors, there is now a dedicated For library authors guide that describes the best practices for building on top of Zod. It answers common questions about how to support Zod 3 & Zod 4 (including Mini) simultaneously.

pnpm upgrade zod@latest

解析愉快!
—— Colin McDonnell @colinhacks