mcpField
W
web-forms-zod-validation

web-forms-zod-validation

github.com/agents-inc开发26163 次安装2 天前更新
代码仓库

关于 web-forms-zod-validation

面向 TypeScript 开发者的 Zod 模式编写指南,涵盖数据验证、类型推导、转换、组合及 v4 迁移,适用于在应用边界校验数据。

主要功能

  1. 01安全解析与错误格式化
  2. 02使用 z.infer 推导类型
  3. 03细化规则与跨字段校验
  4. 04转换及输入输出类型
  5. 05模式组合与联合类型
  6. 06Zod v4 迁移指引

适用场景

  1. 01校验表单输入并报告字段错误
  2. 02检查 API 响应和 URL 参数
  3. 03表达条件变体与跨字段规则
  4. 04将字符串转换为数字或日期

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-forms-zod-validation

安装后在 Claude Code 中输入/web-forms-zod-validation确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
advanced-patterns.md
core.md
transforms.md

详细介绍

Zod 模式验证模式

快速指南: 只需声明一次模式,再用 z.infer 推导 TypeScript 类型,这样规则和类型就不会出现偏差。当无效输入属于预期情况时,safeParse 会返回结果对象;当出现错误就意味着代码有问题时,parse 会抛出异常。refine 和 superRefine 可用于编写内置检查无法表达的规则,而 transform 会在验证期间转换数据,因此 z.input 和 z.output 会有所不同。在 v4 中,字符串格式方法移到了顶层(z.email()、z.url()、z.iso.*),v3 的方法链已弃用,但尚未移除;flatten()、format() 和 merge() 均有替代方法,完整迁移列表见 reference.md。

详细资源:


适用路径

  • 模式检查值并返回收到的值 — 只需使用 z.infer 这一个类型辅助工具,解析后的值与调用方传入的值结构相同。请参阅 examples/core.md。
  • 模式在检查时进行转换 — transform、coerce 或 default 会导致输入和输出类型不同,因此,接收验证前数据的函数应将参数类型标为 z.input,并将返回值类型标为 z.output。请参阅 examples/transforms.md。

<critical_requirements>

编写 Zod 模式前

预期会收到无效输入时,请优先使用 safeParse。 它返回结果对象而不是抛出异常,因此失败会进入一个分支,而不是 catch;result.error.issues 则包含表单所需的字段路径。对于配置和内部数据,请使用 parse,因为无效数据意味着存在错误。

使用 z.infer<typeof schema> 推导类型。 在模式旁手写接口,就相当于对同一事物声明两次;两者会逐渐偏离,最终类型声称的内容比模式实际检查的更多。

在不可信数据进入系统时进行验证——例如 API 响应、表单输入、配置和 URL 参数。在边界处捕获形状变化,可以指出发生变化的字段;如果之后才捕获,就会在几层调用之外暴露为未定义属性。

为验证限制命名。 .min(MIN_USERNAME_LENGTH) 能说明这个数字的用途,同一个常量也会用于错误消息,因此两者不会不一致。

</critical_requirements>


自动检测: zod、z.object、z.infer、z.input、z.output、safeParse、safeParseAsync、parse、parseAsync、refine、superRefine、ctx.addIssue、transform、discriminatedUnion、z.coerce、z.pipe、z.catch、z.brand、z.lazy、z.email、z.url、z.uuid、z.iso、z.flattenError、z.treeifyError、z.strictObject、z.looseObject、ZodError

适用场景:

  • 验证跨越信任边界的数据,并报告验证失败的字段
  • 根据强制执行这些规则的模式推导 TypeScript 类型
  • 跨字段规则、条件形状和判别变体
  • 在验证期间转换值——例如将字符串转为数字、将 ISO 字符串转为 Date
  • 为同一条记录的读取、创建和更新形状组合模式

其他部分负责:

  • 将模式接入表单——表单库可通过自己的适配器或验证器接口接收模式;具体连接方式由负责该部分的组件决定。
  • 数据的来源——模式会验证传入的值,本身不会进行 I/O。
  • 持久化模式——运行时验证器和表定义是不同的产物,即使它们描述的是同一条记录也是如此;如何根据其中一个生成另一个,属于相应工具的职责。

TypeScript 会检查编译的代码;模式会检查接收的数据。两者在边界处相遇,而从模式推导类型的意义就在于,只有其中一个可能出错。

const UserSchema = z.object({ name: z.string(), email: z.email() });
type User = z.infer<typeof UserSchema>;

反过来写——先定义接口,再在旁边维护模式——如果在接口中添加字段却忘了更新模式,代码会在所有地方通过类型检查,却不会验证任何内容。

由此可知,哪些地方_不该_使用模式。已经跨越边界的数据经过了检查;在编译器已能约束的函数内部重新验证它没有任何收益,却会在每次调用时增加一次解析。


核心模式

模式 1:使用命名限制的模式

常量同时出现在检查条件和消息中,因此修改限制时,两处都会同步更新。

const MIN_USERNAME_LENGTH = 3;
const MAX_USERNAME_LENGTH = 50;

const UserSchema = z.object({
  username: z
    .string()
    .min(
      MIN_USERNAME_LENGTH,
      `Username must be at least ${MIN_USERNAME_LENGTH} characters`,
    )
    .max(
      MAX_USERNAME_LENGTH,
      `Username cannot exceed ${MAX_USERNAME_LENGTH} characters`,
    ),
  email: z.email("Invalid email format"),
});

type User = z.infer<typeof UserSchema>;

完整代码:examples/core.md 模式 1


模式 2:safeParse 和错误格式化

结果是按 success 区分的联合类型,因此失败分支会收窄为错误,成功分支则会收窄为类型化数据。

const result = UserSchema.safeParse(data);

if (!result.success) {
  const { fieldErrors } = z.flattenError(result.error);
  return { success: false, errors: fieldErrors };
}

return { success: true, user: result.data };

z.flattenError 会生成一层字段到消息的映射,适用于扁平表单。 z.treeifyError 则会保留更深层的嵌套结构。

完整代码:examples/core.md 模式 2


模式 3:细化与跨字段规则

refine 会为单个值添加条件。superRefine 可以查看整个对象,并选择错误附加到哪个路径,因此不匹配消息会显示在用户需要修正的字段上。

const PasswordFormSchema = z
  .object({
    password: z.string().min(MIN_PASSWORD_LENGTH),
    confirmPassword: z.string(),
  })
  .superRefine((data, ctx) => {
    if (data.password !== data.confirmPassword) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Passwords do not match",
        path: ["confirmPassword"],
      });
    }
  });

完整代码:examples/core.md 模式 5


模式 4:转换与输入/输出类型的区分

架构经过转换后会有两种类型。z.infer 返回输出类型,因此接收原始数据的函数应将参数标注为 z.input。

const DateSchema = z.iso.datetime().transform((str) => new Date(str));

type DateInput = z.input<typeof DateSchema>; // string
type DateOutput = z.output<typeof DateSchema>; // Date

.transform() 会在同一架构上的其他所有检查之后运行,因此,若规则必须检查转换后的值,应将其放在 .pipe() 之后,而不是转换之前。

完整代码:examples/transforms.md 模式 7;.pipe() 示例见模式 9


模式 5:判别联合

如果各个变体共享一个字面量字段,为其命名后,Zod 就能检查对应分支,而不是检查所有分支。

const NotificationSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("email"), email: z.email(), subject: z.string() }),
  z.object({ type: z.literal("sms"), phone: z.string(), message: z.string() }),
  z.object({
    type: z.literal("push"),
    deviceId: z.string(),
    title: z.string(),
  }),
]);

普通的 z.union 会尝试每个成员并报告合并后的失败信息,显示为“无效输入”;判别形式会命名变体及其内部字段,并在 switch 中进行类型收窄。

完整代码:examples/core.md 模式 4


模式 6:组合

使用一个基础架构,并由它派生出读取、创建和更新的形状,这样添加到基础架构中的字段就会应用到这三种形状。

const UserSchema = BaseEntitySchema.extend({
  email: z.email(),
  name: z.string(),
});

const CreateUserSchema = UserSchema.omit({
  id: true,
  createdAt: true,
  updatedAt: true,
});
const UpdateUserSchema = CreateUserSchema.partial();
const UserSummarySchema = UserSchema.pick({ id: true, name: true });

已包含 .refine() 的架构无法使用 .extend(),因此应先扩展,最后再进行精细校验。

完整代码:examples/core.md 模式 3


模式 7:将字符串来源的值强制转换为所需类型

URL 参数和表单字段无论表示什么,传入时都是字符串。z.coerce 会在检查前进行转换,因此规则可以按其实际类型来编写。

const PaginationSchema = z.object({
  page: z.coerce.number().int().positive().default(DEFAULT_PAGE),
  limit: z.coerce
    .number()
    .int()
    .positive()
    .max(MAX_LIMIT)
    .default(DEFAULT_LIMIT),
});

z.coerce.boolean() 等同于 Boolean(value),因此所有非空字符串都会变成 true,包括 "false"。当字符串本身表示意图时,请使用 z.stringbool()。

完整代码:examples/transforms.md 模式 8


模式 8:optional、nullable、nullish 和 default

这四种方式对缺失值表达了不同含义;根据实际含义选择,才能确保类型准确。

const ProfileSchema = z.object({
  name: z.string(), // required
  bio: z.string().optional(), // may be omitted — string | undefined
  avatar: z.url().nullable(), // explicitly empty — string | null
  nickname: z.string().nullish(), // either — string | null | undefined
  theme: z.string().default("light"), // absent becomes "light" — always string
});

.default() 会填充缺失的键,因此输出类型仍为非可选类型,而输入类型则不是——这又是一个说明 z.input 和 z.output 不同的原因。


<red_flags>

注意事项

运行时会出错:

  • 对用户输入使用 parse:在预期情况下会抛出异常,而且 catch 块会在可能读取 error.issues 的分支被跳过后才收到异常。请使用 safeParse。
  • 对包含异步精细校验的架构进行同步解析:Zod 会抛出异常,而不会等待。对于包含异步精细校验的架构,请使用 parseAsync 或 safeParseAsync。
  • 对已包含 .refine() 的架构使用 .extend():精细校验后的架构不再提供该方法。应先扩展,最后再进行精细校验。
  • v4:.refine(fn, (val) => ({ message })):第二个参数的函数形式已移除。若消息取决于值,请使用 superRefine。
  • v4:使用单个参数的 z.record(valueSchema):现在键和值的架构都必需。

行为出乎意料:

  • 未知键会被剥离,而不是拒绝。z.strictObject() 会拒绝这些键,z.looseObject() 会保留它们;普通对象架构会悄悄丢弃这些键,从而隐藏被重命名的 API 字段。
  • z.coerce.boolean() 会将 "false" 强制转换为 true,而 z.coerce.date() 会接受 new Date() 能接受的所有值,包括能解析为日期、但并非预期的字符串。如果输入应为 ISO 格式,z.iso.datetime() 会明确这一要求并拒绝其他格式。
  • z.email() 会拒绝空字符串,因此提交 "" 的可选字段会校验失败。请明确允许空字符串,或在解析前将 "" 规范化为 undefined。
  • 如果 superRefine 读取的键缺失,该规则会被跳过而非校验失败,因此对象仍会通过校验。
  • 检查未设置消息时,默认的 "Invalid input" 消息会原样显示给用户。
  • v4:superRefine 中的 ctx.path 已移除;ctx.addIssue({ path: [...] }) 仍可使用。
  • v4 替代项:.flatten() → z.flattenError(),.format() → z.treeifyError(),.merge() → .extend()。

reference.md 中提供了其中最常见问题的前后对比示例代码。

</red_flags>

评论

支持 Markdown