TypeScript 的类型系统在编译时帮你挡住错误,但 API 响应是在运行时到达的。Zod 补上了这个缺口:它在运行时验证数据的形状和类型,同时从 schema 推导出 TypeScript 类型,一份定义两用。手写复杂 JSON 结构的 Zod schema 很费时间,JSON 转 Zod 工具能在几秒内完成这件事。

Zod 是什么?

Zod 是一个以 TypeScript 为先的 schema 声明与验证库。与 JSON.parse() 返回 any 不同,Zod:

  • 验证数据是否符合预期结构
  • 从 schema 推导 TypeScript 类型(无需重复写类型定义)
  • 生成结构化的详细错误信息
  • 同时支持 Node.js 和浏览器
import { z } from 'zod'

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest'])
})

type User = z.infer<typeof UserSchema>
// 等价于: { id: number; name: string; email: string; role: 'admin' | 'user' | 'guest' }

const result = UserSchema.safeParse(apiResponse)
if (result.success) {
  console.log(result.data.name) // 类型为 string
} else {
  console.error(result.error.issues)
}

JSON 转 Zod 的转换逻辑

给定 JSON 样本:

{
  "id": 42,
  "name": "张三",
  "email": "[email protected]",
  "age": 28,
  "active": true,
  "tags": ["admin", "beta"],
  "address": {
    "province": "广东",
    "city": "深圳",
    "district": "南山区"
  },
  "metadata": null
}

工具生成:

import { z } from 'zod'

const Schema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string(),
  age: z.number(),
  active: z.boolean(),
  tags: z.array(z.string()),
  address: z.object({
    province: z.string(),
    city: z.string(),
    district: z.string()
  }),
  metadata: z.null()
})

export type Schema = z.infer<typeof Schema>

生成的 schema 是起点,后续按业务规则细化(见下文)。

在哪里用 Zod 验证

API 响应验证

// 不用 Zod — any 类型,运行时没有保障
const user = (await fetch('/api/user').then(r => r.json())) as User

// 用 Zod — 验证 + 类型推导
const UserSchema = z.object({ id: z.number(), name: z.string() })

async function fetchUser(id: number) {
  const raw = await fetch(`/api/users/${id}`).then(r => r.json())
  return UserSchema.parse(raw) // 结构不匹配时抛出 ZodError
}

对接微信开放平台、阿里云 API、第三方支付接口时,响应结构复杂且文档不总是准确,Zod 验证能在对齐阶段提前暴露问题。

tRPC / Next.js API 路由

import { z } from 'zod'
import { publicProcedure } from '../trpc'

const createUserInput = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'user']).default('user')
})

export const createUser = publicProcedure
  .input(createUserInput)
  .mutation(async ({ input }) => {
    // input 类型为 { name: string; email: string; role: 'admin' | 'user' }
    return db.users.create(input)
  })

表单验证(React Hook Form)

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const formSchema = z.object({
  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'),
  password: z.string().min(8, '至少 8 位'),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: '两次密码不一致',
  path: ['confirmPassword']
})

type FormData = z.infer<typeof formSchema>

function RegisterForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(formSchema)
  })
  // ...
}

环境变量校验

import { z } from 'zod'

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().int().positive().default(3000),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  API_SECRET: z.string().min(32)
})

export const env = envSchema.parse(process.env)
// 启动时若环境变量缺失或格式错误,直接崩溃并输出清晰报错

细化生成的 Schema

工具从 JSON 值推断类型,但 JSON 无法表达所有 Zod 约束,这些需要手动补充:

字符串格式

// 工具生成
email: z.string()

// 细化后
email: z.string().email()
url: z.string().url()
uuid: z.string().uuid()
isoDate: z.string().datetime()
phone: z.string().regex(/^1[3-9]\d{9}$/)

数字约束

// 工具生成
age: z.number()

// 细化后
age: z.number().int().min(0).max(150)
price: z.number().positive()
quantity: z.number().int().nonnegative()

可选字段

JSON 样本只反映有值的字段,实际可能缺失:

// 样本中存在该字段,工具生成
nickname: z.string()

// 细化后
nickname: z.string().optional()   // 允许 undefined
nickname: z.string().nullable()   // 允许 null
nickname: z.string().nullish()    // 允许 null 或 undefined

枚举和联合类型

// 样本中 status 是 "active",工具生成
status: z.string()

// 掌握全部枚举值后细化
status: z.enum(['active', 'inactive', 'pending', 'archived'])

// 不同形状的联合
result: z.union([
  z.object({ success: z.literal(true), data: DataSchema }),
  z.object({ success: z.literal(false), error: z.string() })
])

null 值的处理

JSON 字段为 null 时,工具生成 z.null()。实际含义通常是该字段可为 null,而非永远为 null:

// 工具生成
metadata: z.null()

// 更实用的写法
metadata: z.string().nullable()               // string | null
metadata: z.record(z.unknown()).nullable()    // object | null

parse 与 safeParse

方法失败时行为返回值
.parse(data)抛出 ZodError验证通过的数据
.safeParse(data)返回 { success: false, error }{ success, data | error }

启动阶段用 .parse()(环境变量、配置文件),失败应该直接崩溃。处理 API 响应和用户输入时用 .safeParse(),优雅处理错误。

// parse — 失败即崩溃
const config = ConfigSchema.parse(process.env)

// safeParse — 优雅处理
const result = UserSchema.safeParse(apiResponse)
if (!result.success) {
  return { error: result.error.format() }
}
// result.data 完整类型推导
return result.data

使用工具

在线 JSON 转 Zod Schema →

粘贴 API 响应样本、Postman 示例、Swagger 定义中的 JSON,立即获得可运行的 Zod schema。支持嵌套对象、数组、null 值和混合类型数组,输出包含 z.infer 类型导出,无需额外写 TypeScript interface。