• 微信:WANCOME
  • 扫码加微信,提供专业咨询
  • 服务热线
  • 13215191218
    13027920428

  • 微信扫码访问本页
desc-2
环企首页

Zod

概述

Zod 是一个 TypeScript 优先的运行时数据校验库。通过声明式的 Schema 定义,在运行时验证数据是否符合预期,同时自动推导 TypeScript 类型。核心特点:

  • 类型推导:Schema 即类型定义,无需重复写 TS 类型
  • 链式 API:易读易写
  • 零依赖:体积小
  • 全能校验:字符串、数字、对象、数组、枚举、递归类型全覆盖

适用场景:

  • API 请求参数校验(req.body、query、params)
  • 环境变量校验(启动前检查)
  • 配置文件校验
  • 表单数据校验

vs Joi: Zod 天然支持 TypeScript 类型推导,Joi 需要额外维护类型。

核心函数

创建 Schema

const { z } = require('zod')

// 字符串
const nameSchema = z.string()
const emailSchema = z.string().email()           // 邮箱格式
const urlSchema = z.string().url()               // URL 格式
const uuidSchema = z.string().uuid()             // UUID 格式
const regexSchema = z.string().regex(/^\d{11}$/) // 正则
const minLenSchema = z.string().min(2)           // 最短长度
const maxLenSchema = z.string().max(100)
const lenSchema = z.string().length(11)          // 固定长度

// 数字
const ageSchema = z.number()
const intSchema = z.number().int()               // 整数
const positiveSchema = z.number().positive()     // > 0
const nonNegativeSchema = z.number().nonnegative() // >= 0
const rangeSchema = z.number().min(0).max(150)  // 范围

// 布尔
const boolSchema = z.boolean()

// 枚举
const statusSchema = z.enum(['pending', 'active', 'done'])

// 对象
const userSchema = z.object({
  name: z.string().min(2),
  age: z.number().min(0).max(150).optional(),   // 可选
  email: z.string().email(),
  role: z.enum(['admin', 'user']).default('user') // 带默认值
})

// 数组
const idsSchema = z.array(z.number())
const stringsSchema = z.array(z.string()).min(1).max(10)

// 日期
const dateSchema = z.date()

// 联合类型
const statusOrNumSchema = z.union([z.enum(['A', 'B']), z.number()])

// 互斥(满足其一)
const optionalStringSchema = z.string().optional().or(z.null())

// 深度可选(对象所有字段变可选)
const partialUserSchema = userSchema.partial()

// 深度必填(对象所有字段变必填)
const requiredUserSchema = userSchema.required()

// 读取-only(去除某些字段)
const publicUserSchema = userSchema.omit({ passwordHash: true })
const publicUserSchema2 = userSchema.pick({ name: true, email: true })

校验数据 — .parse() / .safeParse()

// parse — 校验失败抛异常(ZodError)
try {
  const user = userSchema.parse({
    name: '张三',
    age: 25,
    email: 'zhangsan@example.com',
    role: 'admin'
  })
  // user 是完整类型推断的 User 类型
} catch (err) {
  if (err instanceof z.ZodError) {
    console.log(err.errors)   // [{ path: [...], message: '...' }]
  }
}

// safeParse — 返回结果对象,不抛异常(推荐)
const result = userSchema.safeParse({
  name: '张',
  email: 'not-an-email',
  age: 25
})

if (result.success) {
  console.log(result.data)    // 校验通过的数据
} else {
  console.log(result.error.issues)
  // [
  //   { path: ['name'], message: 'String must contain at least 2 character(s)' },
  //   { path: ['email'], message: 'Invalid email' }
  // ]
}

// safeParseAsync — 异步校验(用于异步 transform)
const asyncResult = await userSchema.safeParseAsync(data)

类型推导

// 直接从 Schema 推导 TypeScript 类型(无需重复定义)
type User = z.infer<typeof userSchema>
// 等价于:
// {
//   name: string;
//   age?: number;
//   email: string;
//   role: 'admin' | 'user';
// }

// 在 API 路由中使用
app.post('/users', async (req, res) => {
  const result = userSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({ errors: result.error.errors })
  }
  // result.data 已经是完全类型安全的 User 类型
  const user = await createUser(result.data)
  res.status(201).json(user)
})

自定义错误信息

const schema = z.string({
  required_error: '必填字段',
  invalid_type_error: '类型错误'
})

// 每个方法可单独设置 message
const nameSchema = z.string().min(2, '名字至少2个字符')
const ageSchema = z.number().min(18, '必须年满18周岁').max(150, '年龄不合法')

// 自定义校验函数
const customSchema = z.string().refine(
  (val) => val.startsWith('admin_'),           // 校验函数
  { message: '必须以 admin_ 开头' }            // 失败时的错误信息
)

// 带条件的校验
const passwordSchema = z.string()
  .min(8, '密码至少8位')
  .refine(
    (val) => /[A-Z]/.test(val) && /[0-9]/.test(val),
    { message: '密码必须包含大小写字母和数字' }
  )

Transform(转换)

// 数据转换(校验通过后自动转换)
const intSchema = z.string().transform((val) => parseInt(val, 10))

const result = intSchema.safeParse('42')
// result.success === true, result.data === 42

// pipe — 链式转换
const trimAndLower = z.string().trim().toLowerCase()

// 复杂转换(带副作用)
const dateSchema = z.string()
  .transform((str) => new Date(str))
  .refine((date) => date < new Date(), {
    message: '日期不能是未来'
  })

预处理(Preprocess)

用于在解析前先处理数据(常用于处理 JSON.parse 字符串)。

// 处理前端传来的 JSON 字符串
const requestSchema = z.object({
  data: z.preprocess(
    (val) => typeof val === 'string' ? JSON.parse(val) : val,
    z.object({ id: z.number(), name: z.string() })
  )
})

// 统一处理空字符串为 undefined
const optionalString = z.preprocess(
  (val) => val === '' ? undefined : val,
  z.string().optional()
)

// 批量预处理(coerce)
const coerceInt = z.coerce.number()  // 自动将 '42' 转为 42
const coerceBool = z.coerce.boolean() // ''/null/undefined → false,'true'/'1' → true

嵌套对象与数组

const postSchema = z.object({
  id: z.number(),
  title: z.string().min(1).max(200),
  tags: z.array(z.string()).min(1),
  author: z.object({
    id: z.number(),
    name: z.string(),
    profile: z.object({
      avatar: z.string().url().optional()
    }).optional()
  }),
  comments: z.array(
    z.object({
      id: z.number(),
      content: z.string().min(1),
      createdAt: z.string().datetime()
    })
  ).optional()
})

// 解析
const post = postSchema.parse({ ... })

常见集成模式

// Express 集成
app.post('/users', async (req, res) => {
  const result = userSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({
      error: '参数错误',
      details: result.error.errors.map(e => ({
        field: e.path.join('.'),
        message: e.message
      }))
    })
  }
  const { name, email, age } = result.data
  // ...
})

// 环境变量校验(启动时)
const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production']).default('development'),
  PORT: z.coerce.number().min(1).max(65535).default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32)
})

const env = envSchema.safeParse(process.env)
if (!env.success) {
  console.error('环境变量配置错误:')
  console.error(env.error.errors)
  process.exit(1)
}

// 全局访问已校验的环境变量
const config = env.data

// Fastify 集成
fastify.post('/users', {
  schema: {
    body: userSchema
  }
}, handler)

常见问题

问题 解决方案
校验对象时有未知字段 默认忽略,用 strict 模式报错
想要可选字段为空字符串时自动转为 undefined z.preprocess
递归类型(如树形结构) z.lazy()z.recursive()
校验通过但类型不对 z.infer 推导,不用重复定义
错误信息太笼统 每个方法单独设置 message