🚀 3分钟秒懂:Java 视角下的 Zod
把 Zod 理解为:DTO 结构 + Bean Validation + TS 类型声明 的三合一工具。它不是 ORM,只是个运行时数据校验器。
1. 概念对齐(Java vs Zod)
| Java 概念 | Zod 对应写法 | 说明 |
|---|---|---|
| 定义 DTO 类 | z.object({...}) | 结构与规则写在一起 |
@NotBlank / @Size | z.string().min(1) | 基础格式校验 |
@Pattern(regexp=...) | z.string().regex(...) | 正则校验 |
@Positive | z.number().positive() | 数值校验 |
| 手动触发校验(抛异常) | schema.parse(data) | 失败直接 throw Exception |
| 校验并获取结果对象 | schema.safeParse(data) | 返回 { success: true/false } 不抛异常 |
⚠️ 核心注意点:Zod 只是规则定义,不会自动拦截!必须显式调用
parse()或safeParse()才会触发校验。
2. 核心大杀器:同步 vs 异步
- 同步校验 (
parse/safeParse):只检查格式(非空、数字、长度、邮箱)。 - 异步校验 (
parseAsync/safeParseAsync):用于检查业务。允许在校验时查数据库、调接口(例如:校验“银行账号是否存在”、“会计期间是否已关闭”)。
3. 高级进阶:复杂业务与类型联动
refine/superRefine**:相当于 Java 的自定义校验器(ConstraintValidator)**。适合写财务里的复杂逻辑(如:校验借贷平衡、单据总金额对不对)。z.infer(前端降维打击):
typescript
type Voucher = z.infer<typeof voucherSchema>;
单一真理源:写一份 Schema,自动反推出 TypeScript 的类型声明,再也不用重复写一遍 DTO 类了。
🛠️ 常用 API 备忘表
| 类别 | Zod API | Java 对应概念 |
|---|---|---|
| 容器 | z.object({...}) | public class MyDto { ... } |
z.array(z.string()) | List<String> | |
| 基础 | z.string().min(1) | @NotBlank |
z.number().positive() | @Positive | |
z.boolean() | Boolean | |
z.enum(["CNY", "USD"]) | enum Currency { CNY, USD } | |
| 修饰 | z.string().optional() | 允许为 undefined (类似 null) |
💻 极简 Demo 演示
以财务系统里最经典的“凭证录入(Voucher)”为例,包含结构定义、基础校验、自定义借贷平衡校验(Refine)以及类型反推:
typescript
import { z } from 'zod';
// 1. 定义 Schema(相当于写 Java DTO + 校验注解)
const voucherSchema = z.object({
id: z.string().uuid(), // UUID 格式
period: z.string().regex(/^\d{4}-\d{2}$/), // 格式如 "2026-06"
currency: z.enum(["CNY", "USD"]), // 枚举限制
// 分录列表(相当于 List<Entry>),至少 2 条分录
entries: z.array(
z.object({
accountCode: z.string().min(1), // 科目非空
debit: z.number().nonnegative(), // 借方金额 >= 0
credit: z.number().nonnegative(), // 贷方金额 >= 0
})
).min(2)
}).superRefine((data, ctx) => {
// 复杂的业务校验:借贷平衡(类似 Java 的自定义 ConstraintValidator)
const totalDebit = data.entries.reduce((sum, e) => sum + e.debit, 0);
const totalCredit = data.entries.reduce((sum, e) => sum + e.credit, 0);
if (totalDebit !== totalCredit) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `借贷不平衡!借方:${totalDebit}, 贷方:${totalCredit}`,
path: ['entries'] // 错误挂在 entries 字段上
});
}
});
// 2. 🪄 降维打击:一键反推出 TypeScript 类型(不需要手写 interface 啦!)
type Voucher = z.infer<typeof voucherSchema>;
// 3. 运行期测试数据
const badData = {
id: "not-a-uuid",
period: "2026/06",
currency: "HKD", // 不在枚举内
entries: [
{ accountCode: "1001", debit: 100, credit: 0 },
{ accountCode: "2001", debit: 0, credit: 90 } // 借贷不平衡
]
};
// 4. 执行校验
// 方案 A:直接抛异常(类似 Spring 默认行为)
// voucherSchema.parse(badData);
// 方案 B:优雅获取结果对象(适合 CLI 或业务层返回 Result)
const result = voucherSchema.safeParse(badData);
if (!result.success) {
// 打印格式化后的错误信息
console.log("❌ 校验失败:", result.error.format());
} else {
// 校验成功后,data 的类型自动安全推导为 Voucher
const validData: Voucher = result.data;
console.log("✅ 校验通过:", validData);
}
想法或问题?在 GitHub Issue 下方参与讨论
去评论