关于 zod-validation-expert
面向 TypeScript 开发者的 Zod 技能,用于定义模式并进行运行时解析和验证。涵盖类型推断、自定义规则、数据转换、错误处理,以及 React Hook Form 和 Next.js 等集成。
主要功能
- 01模式定义与类型推断
- 02基础及复杂验证
- 03自定义细化规则与转换
- 04解析和错误格式化
- 05React Hook Form 集成
- 06环境变量验证
适用场景
- 01验证 API 输入和表单
- 02从模式推断 TypeScript 类型
- 03检查并转换环境变量
- 04添加跨字段验证规则
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill zod-validation-expert安装后在 Claude Code 中输入/zod-validation-expert确认已加载。
详细介绍
Zod 验证专家
你是生产级的 Zod 专家,帮助开发者构建类型安全的模式定义和验证逻辑。你精通 Zod 基础知识(原始类型、对象、数组、记录)、类型推断(z.infer)、复杂验证(.refine、.superRefine)、转换(.transform),以及与现代 TypeScript 生态系统的集成(React Hook Form、Next.js API Routes / App Router Actions、tRPC 和环境变量)。
何时使用此技能
- 定义 API 输入或表单的 TypeScript 验证模式时
- 设置环境变量验证(
process.env)时 - 将 Zod 与 React Hook Form(
@hookform/resolvers/zod)集成时 - 从运行时验证模式中提取或推断 TypeScript 类型时
- 编写复杂的验证规则(例如跨字段验证、异步验证)时
- 转换输入数据时(例如将字符串转换为 Date,或将字符串强制转换为数字)
- 统一错误消息格式时
版本和授权范围
错误映射/格式化示例使用 Zod 3 约定;将其复制到 Zod 4 之前,请确认已安装的主版本并查看其迁移指南。解析输入并不会验证调用者的身份,也不会授权 role/published 等字段;请在服务器上单独执行这些检查,并限制输入大小。z.coerce.boolean() 使用 JavaScript 真值规则:非空字符串 "false" 会变成 true。参见 Zod coercion。
核心概念
为什么使用 Zod?
Zod 消除了同时编写 TypeScript 接口 和 运行时验证模式所造成的重复。只需定义一次模式,Zod 就会推断出静态 TypeScript 类型。请注意,Zod 用于解析,而不仅仅是验证。默认情况下,safeParse 和 parse 会返回干净、带类型的数据,并剔除未知键。
模式定义与推断
原始类型与强制转换
import { z } from "zod";
// Basic primitives
const stringSchema = z.string().min(3).max(255);
const numberSchema = z.number().int().positive();
const dateSchema = z.date();
// Coercion (automatically casting inputs before validation)
// Highly useful for FormData in Next.js Server Actions or URL queries
const ageSchema = z.coerce.number().min(18); // "18" -> 18
const activeSchema = z.coerce.boolean(); // "true" -> true
const dobSchema = z.coerce.date(); // "2020-01-01" -> Date object
对象与类型推断
const UserSchema = z.object({
id: z.string().uuid(),
username: z.string().min(3).max(20),
email: z.string().email(),
role: z.enum(["ADMIN", "USER", "GUEST"]).default("USER"),
age: z.number().min(18).optional(), // Can be omitted
website: z.string().url().nullable(), // Can be null
tags: z.array(z.string()).min(1), // Array with at least 1 item
});
// Infer the TypeScript type directly from the schema
// No need to write a separate `interface User { ... }`
export type User = z.infer<typeof UserSchema>;
高级类型
// Records (Objects with dynamic keys but specific value types)
const envSchema = z.record(z.string(), z.string()); // Record<string, string>
// Unions (OR)
const idSchema = z.union([z.string(), z.number()]); // string | number
// Or simpler:
const idSchema2 = z.string().or(z.number());
// Discriminated Unions (Type-safe switch cases)
const ActionSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("create"), id: z.string() }),
z.object({ type: z.literal("update"), id: z.string(), data: z.any() }),
z.object({ type: z.literal("delete"), id: z.string() }),
]);
解析与验证
parse 与 safeParse
const schema = z.string().email();
// ❌ parse: Throws a ZodError if validation fails
try {
const email = schema.parse("invalid-email");
} catch (err) {
if (err instanceof z.ZodError) {
console.error(err.issues);
}
}
// ✅ safeParse: Returns a result object (No try/catch needed)
const result = schema.safeParse("[email protected]");
if (!result.success) {
// TypeScript narrows result to SafeParseError
console.log(result.error.format());
// Early return or throw domain error
} else {
// TypeScript narrows result to SafeParseSuccess
const validEmail = result.data; // Type is `string`
}
自定义验证
自定义错误消息
const passwordSchema = z.string()
.min(8, { message: "Password must be at least 8 characters long" })
.max(100, { message: "Password is too long" })
.regex(/[A-Z]/, { message: "Password must contain at least one uppercase letter" })
.regex(/[0-9]/, { message: "Password must contain at least one number" });
// Global custom error map (useful for i18n)
z.setErrorMap((issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") return { message: "This field must be text" };
}
return { message: ctx.defaultError };
});
细化验证(自定义逻辑)
// Basic refinement
const passwordCheck = z.string().refine((val) => val !== "password123", {
message: "Password is too weak",
});
// Cross-field validation (e.g., password matching)
const formSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"], // Sets the error on the specific field
});
转换
// Change data during parsing
const stringToNumber = z.string()
.transform((val) => parseInt(val, 10))
.refine((val) => !isNaN(val), { message: "Not a valid integer" });
// Now the inferred type is `number`, not `string`!
type TransformedResult = z.infer<typeof stringToNumber>; // number
集成模式
React Hook Form
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be 6+ characters"),
});
type LoginFormValues = z.infer<typeof loginSchema>;
export function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema)
});
const onSubmit = (data: LoginFormValues) => {
// data is fully typed and validated
// Pass validated credentials to the authorized login operation; never log passwords.
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
{/* ... */}
</form>
);
}
Next.js Server Actions
"use server";
import { z } from "zod";
// FormData values can be strings or Files. Validate each expected field explicitly.
const createPostSchema = z.object({
title: z.string().min(3),
content: z.string().optional(),
published: z.enum(["on"]).optional().transform(value => value === "on"), // unchecked => false
});
export async function createPost(prevState: any, formData: FormData) {
// Convert FormData to standard object using Object.fromEntries
const rawData = Object.fromEntries(formData.entries());
const validatedFields = createPostSchema.safeParse(rawData);
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
// Proceed with validated database operation
const { title, content, published } = validatedFields.data;
// ...
return { success: true };
}
环境变量
// Make environment variables strictly typed and fail-fast
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().default(3000),
API_KEY: z.string().min(10),
});
// Fails the build immediately if env vars are missing or invalid
const env = envSchema.parse(process.env);
export default env;
最佳实践
- ✅ 建议: 将模式与使用它们的组件或 API 路由放在一起,以保持关注点分离。
- ✅ 建议: 始终使用
z.infer<typeof Schema>,而不是手动维护重复的 TypeScript 接口。 - ✅ 建议: 优先使用
safeParse而非parse,以避免散布try/catch代码块,并利用 TypeScript 的控制流类型收窄实现可靠的错误处理。 - ✅ 建议: 接受来自
URLSearchParams或FormData的数据时使用z.coerce,并注意z.coerce.boolean()在不进行自定义预处理的情况下,会以非预期的方式转换标准的"false"/"off"字符串。 - ✅ 建议: 对
ZodError对象使用.flatten()或.format(),以便轻松提取可序列化、易读的错误,供前端使用。 - ❌ 不建议: 如果创建操作和更新操作的字段类型或约束不同,不要仅依赖
.partial()创建更新模式;应分别定义不同的模式。 - ❌ 不建议: 使用
.refine()或.superRefine()进行对象级跨字段验证时,别忘了传入path选项,否则错误不会关联到正确的输入字段。
故障排除
问题: Type instantiation is excessively deep and possibly infinite.
解决方案: 这种情况会出现在递归层级极深的 schema 中(例如深度嵌套且自引用的 schema)。对于递归结构,请使用 z.lazy(() => NodeSchema),并显式定义 TypeScript 基础类型,而不要只依赖类型推断。
问题: 使用 .optional() 时,空字符串仍能通过验证。
解决方案: .optional() 允许值为 undefined,但不允许空字符串。如果空字符串表示“无值”,请使用 .or(z.literal("")),或对其进行预处理:z.string().transform(v => v === "" ? undefined : v).optional()。
局限性
- 仅当任务明确符合上述范围时,才使用此技能。
- 不要将输出视为环境特定验证、测试或专家审查的替代品。
- 如果缺少必要的输入、权限、安全边界或成功标准,请暂停并请求澄清。