使用 Zod 进行表单校验

三葉Leaves Author

官网:Intro | Zod

Note

Zod 是一个现代化、强大的,用于表单校验的库,其设计能很好的支持服务端渲染等现代化的未来场景,同时更是广泛支持各种框架和组件库,他被 NextJS 官方推荐使用。

我认为 zod 的两个最大优点:

  1. 不止是运行时的数据校验,也提供开发时的类型安全(请见本文下面的:[[#3. 推断类型]])
  2. 一次声明,处处验证。定义好一次后,可以给客户端的 React Hook Form 使用,也可以给服务端做校验,成为唯一真相来源,根本解决了同步问题。

使用

1. 定义蓝图

schema 这个词有结构或者蓝图的意思,我们定义好一个数据的 schema,其实就是说明它应该“长什么样”。

Zod 的核心是一个 z 对象,它提供了创建各种数据类型 schema 的方法。

1
2
3
4
5
6
7
8
9
10
import { z } from 'zod';

// 定义一个字符串 schema
const myString = z.string();

// 定义一个数字 schema
const myNumber = z.number();

// 定义一个布尔值 schema
const myBoolean = z.boolean();

是不是非常直观?z.string() 就创建了一个期望数据是字符串的 schema。

schema 里应该定义些什么?

不同于数据库里的数据结构,这里 schema 定义的类型只关注用户输入的那部分。

事实上,下面三种数据结构不同,也应该做区分:

  • 存在数据库里的数据,可能包括每个条目的 UUID 等。
  • 展示给用户看的数据,比如 Todo List 的完成/未完成这个布尔值,这个是隐式的。
  • 用户填的数据,比如 Todo List 里用户真正定义的可能只有 content 。(这才是 Zod 所需要定义的)

schema 的代码应该放在哪里?

我推荐使用现代前端社区推荐的 ducks 风格,这是一种高内聚、低耦合的组织代码的方式,他利用“切片,slice” 的概念,把同一部分逻辑全部放在一起。

例如在 todo list 案例中,我决定在 src\features\todos\schema.ts 里创建一个定义新增 todo 表单类型的 schema 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import z from "zod";

export const todoSchema = z.object({

  content: z

    .string()

    .min(3, { message: "内容不能少于3个字符" })

    .max(50, { message: "内容不能超过50个字符" }),

});

export type todoSchemaType = z.infer<typeof todoSchema>

如果是全局都能用到的 schema,在 src\schema 里创建 todos.ts 也许会是更好的选择。

2. 通过安检门

Zod 提供了两个主要的方法:.parse() 和 .safeParse()。

这两个方法的返回值,是经过校验的、可以真正传给后端或者使用的东西,因为经过这一道门,我们总能确信数据是正确的了, 就像一个安检门一样。

.parse(data)

这个方法会验证 data 是否符合 schema。如果符合,它会返回被验证过的数据;如果不符合,它会直接抛出一个错误

1
2
3
4
5
6
7
8
9
10
const nameSchema = z.string();

const result = nameSchema.parse("张三");
console.log(result); // result 的值是 "张三"

try {
nameSchema.parse(123); // 这里会抛出错误
} catch (error) {
console.error(error); // ZodError: [ { "code": "invalid_type", ... } ]
}

.parse() 适合用在那些你非常确定数据格式,或者希望在格式错误时直接中断程序流程的场景。

.safeParse(data) 🌟

这个是最常用的方法,它极其适合配合 UI 优雅的处理用户输入合法性校验问题。

  • 如果验证成功,返回 { success: true, data: validatedData }。

  • 如果验证失败,返回 { success: false, error: ZodError }。

1
2
3
4
5
6
7
8
9
10
11
12
const emailSchema = z.string().email(); // Zod 内置了 email 格式校验

const result1 = emailSchema.safeParse("hello@example.com");
if (result1.success) {
console.log("验证成功:", result1.data); // result1.data 是 string 类型
}

const result2 = emailSchema.safeParse("invalid-email");
if (!result2.success) {
// result2.error 是一个 ZodError 对象,包含了详细的错误信息
console.log("验证失败:", result2.error.flatten());
}

由于即使是成功,返回的也是一个对象,所以我们需要额外注意使用 result.data 来读到实际的数据。

更多强大的能力

  1. 链式 API: Zod 提供了丰富且可链式调用的 API(.min, .optional, .email 等),让 schema 定义既简洁又强大。
    比如可以像这样连着调用:
    1
    const tagsSchema = z.string().array().min(1, "至少需要一个标签");
    这定义一个由该 schema 元素组成的数组。
  2. 组合与扩展: 通过 .object, .array, .union, .extend 等方法,可以构建出任意复杂的数据结构。

3. 推断类型

这是一个大有益于 IDE 类型推断,以此提升开发体验的,有关于 typescript 的事情。

核心是 z.infer<typeof yourSchema> 方法,其中的 yourSchema 就是你在第一步中定义的 schema 。

这个方法产出的类型,可以在之后的开发中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { z } from 'zod';

// 1. 定义 Schema
const UserSchema = z.object({
username: z.string().min(3, "用户名至少需要3个字符"), // 链式调用添加校验规则
email: z.string().email("请输入有效的邮箱地址"),
age: z.number().optional(), // 表示该字段是可选的
isAdmin: z.boolean().default(false), // 可以设置默认值
});

// 2. 使用 z.infer 推断类型
type User = z.infer<typeof UserSchema>;

/*
鼠标悬停在 User 类型上,你会看到 TypeScript 已经知道了它的结构:
type User = {
username: string;
email: string;
age?: number | undefined; // optional() 使其变为可选
isAdmin: boolean; // default() 使其变为必选,因为总会有个值
}
*/

// 3. 现在你可以放心地使用这个类型了
const user: User = {
username: "johndoe",
email: "john.doe@example.com",
isAdmin: true, // 你可以不提供 isAdmin,它会默认为 false
};

// 验证这个 user 对象
const validationResult = UserSchema.safeParse(user);
if (validationResult.success) {
console.log("用户数据合法!", validationResult.data);
}

4. 结合使用

你可以在这里看到 zod 的生态:

Intro | Zod

最常见的是结合 React Hook Form 使用:

1
pnpm install @hookform/resolvers zod
  • 标题: 使用 Zod 进行表单校验
  • 作者: 三葉Leaves
  • 创建于 : 2025-09-05 00:00:00
  • 更新于 : 2025-09-05 11:19:12
  • 链接: https://blog.oksanye.com/9e7b775c2e0c/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论