mcpField
Z
zod-validation-expert

zod-validation-expert

github.com/sickn33开发48.7k223 次安装今天更新
代码仓库

关于 zod-validation-expert

面向 TypeScript 开发者的 Zod 技能,用于定义模式并进行运行时解析和验证。涵盖类型推断、自定义规则、数据转换、错误处理,以及 React Hook Form 和 Next.js 等集成。

主要功能

  1. 01模式定义与类型推断
  2. 02基础及复杂验证
  3. 03自定义细化规则与转换
  4. 04解析和错误格式化
  5. 05React Hook Form 集成
  6. 06环境变量验证

适用场景

  1. 01验证 API 输入和表单
  2. 02从模式推断 TypeScript 类型
  3. 03检查并转换环境变量
  4. 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()。

局限性

  • 仅当任务明确符合上述范围时,才使用此技能。
  • 不要将输出视为环境特定验证、测试或专家审查的替代品。
  • 如果缺少必要的输入、权限、安全边界或成功标准,请暂停并请求澄清。

评论

支持 Markdown