Zod 4 Schema 校验实战指南:Prowler UI 中的 v3 → v4 迁移与表单校验模式 📅 发布时间:2026/9/15 6:22:54 👁 浏览次数: Zod 4 Schema 校验实战指南Prowler UI 中的 v3 → v4 迁移与表单校验模式【免费下载链接】prowlerProwler is the world’s most widely used open-source cloud security platform that automates security and compliance across any cloud environment.项目地址: https://gitcode.com/GitHub_Trending/pr/prowlerZod 4 带来了全新的顶层校验 APIz.email()、z.uuid()、z.url()、更统一的错误参数约定error取代message以及对 TypeScript 类型推断的全面增强。本指南以 Prowler UIui 目录中实际使用的 Zod 4.4.3 为背景系统梳理 v3 → v4 的破坏性变更、各类 Schema 的编写模式并结合仓库内真实代码表单校验、Server Actions 参数校验、动态查询构建器给出可直接落地的实战方案。读完本文你将掌握 Zod 4 的标准校验写法、错误处理约定以及它与 React Hook Form 的组合方式。说明本文所述版本、API 与代码示例均以当前仓库实际内容为准。Prowler UI 在 ui/package.json 中固定依赖zod: 4.4.3并与react-hook-form7.62.0搭配使用ui/AGENTS.md 将本主题列为创建 Zod Schema 时的强制参考技能并明确要求使用新 APIz.email()、z.uuid()。Zod 3 → Zod 4 破坏性变更速览Zod 4 对日常使用最直观的改变是把字符串格式校验从z.string()的链式方法提升为顶层校验器top-level validators。下表对比了二者写法// ❌ Zod 3旧写法 z.string().email() z.string().uuid() z.string().url() z.string().nonempty() z.object({ name: z.string() }).required_error(Required) // ✅ Zod 4新写法 z.email() z.uuid() z.url() z.string().min(1) z.object({ name: z.string() }, { error: Required })迁移时需注意四个关键差异格式校验器顶层化z.string().email()改为z.email()uuid、url同理。Prowler UI 的 Server Actions 中随处可见这一写法例如 ui/actions/users/users.ts 里的email: z.email().optional()。nonempty()被移除统一用z.string().min(1)表达非空字符串。错误参数统一required_error等专用参数被废弃统一改为第二个参数{ error: ... }。message与error并存但推荐后者Zod 4 中{ message }仍可用于refine/superRefine等场景但在基础校验器如z.string({ ... })、z.min中应使用{ error }。仓库代码对二者都有使用基础约束用error见 ui/types/formSchemas.ts 的min(1, { error: Configuration is required })refine的回调里用message见 ui/types/authFormSchema.ts 的{ message: Password must contain at least one number. }。基础 Schema 与顶层校验器Zod 4 的基础类型写法与 v3 一致但新增了顶层校验器使语义更清晰import { z } from zod; // Primitives基础类型 const stringSchema z.string(); const numberSchema z.number(); const booleanSchema z.boolean(); const dateSchema z.date(); // Top-level validatorsZod 4 顶层校验器 const emailSchema z.email(); const uuidSchema z.uuid(); const urlSchema z.url(); // With constraints带约束 const nameSchema z.string().min(1).max(100); const ageSchema z.number().int().positive().max(150); const priceSchema z.number().min(0).multipleOf(0.01);在 Prowler UI 中这类基础校验被大量用于 Server Action 的入参边界。例如 ui/actions/users/users.ts 的分页查询参数 Schemaconst getUsersSchema z.object({ page: z.coerce.number().int().min(1).default(1), query: z.string().default(), sort: z.string().optional().default(), filters: z .record( z.string(), z.union([z.string(), z.array(z.string()), z.number()]).optional(), ) .default({}), pageSize: z.coerce.number().int().min(1).default(10), });这段代码同时演示了三个高价值模式z.coerce.number()把来自 URL/表单的字符串自动转换为数字.int().min(1)约束整数下限.default(...)提供缺省值保证解析后的对象字段永远齐全。对象 Schema 与类型推断对象 Schema 是表单校验的核心载体配合z.infer可以零成本获得完整的 TypeScript 类型const userSchema z.object({ id: z.uuid(), email: z.email({ error: Invalid email address }), name: z.string().min(1, { error: Name is required }), age: z.number().int().positive().optional(), role: z.enum([admin, user, guest]), metadata: z.record(z.string(), z.unknown()).optional(), }); type User z.infertypeof userSchema; // Parsing const user userSchema.parse(data); // 校验失败直接抛异常 const result userSchema.safeParse(data); // 返回 { success, data/error } if (result.success) { console.log(result.data); } else { console.log(result.error.issues); }z.infer与z.input的差异值得注意z.infer给出的是解析后的输出类型经过 transform/coerce 之后而z.input给出的是输入类型。在 ui/actions/users/users.ts 中二者被明确区分使用type GetUsersInput z.inputtypeof getUsersSchema; // 输入侧类型 type UpdateUserData z.infertypeof updateUserSchema; // 输出侧类型 type UserAttributes OmitUpdateUserData, userId;Prowler UI 的告警表单 alert-form-schema.ts/alerts/_lib/alert-form-schema.ts#L13-L23) 是对象 Schema 的完整范例值得逐行解读export const alertFormSchema z.object({ name: z.string().trim().min(1, { error: Name is required. }).max(120), description: z.string().trim().max(2000).default(), frequency: z.enum(ALERT_TRIGGER_KIND_VALUES), condition: alertConditionSchema, // z.customAlertCondition recipientEmails: z .array(z.email({ error: Enter a valid email address. })) .default([]), slackChannels: z.array(z.string().trim().min(1)).default([]), enabled: z.boolean(), });其中z.enum(ALERT_TRIGGER_KIND_VALUES)直接接收从常量数组推导出的类型——与 ui/AGENTS.md 中用as const定义常量并用typeof X[keyof typeof X]推导联合类型的约定完全吻合。z.customAlertCondition则用于把自定义业务类型接入校验体系。数组、记录与元组// Arrays数组 const tagsSchema z.array(z.string()).min(1).max(10); const numbersSchema z.array(z.number()).nonempty(); // Records动态键对象 const scoresSchema z.record(z.string(), z.number()); // { [key: string]: number } // Tuples元组 const coordinatesSchema z.tuple([z.number(), z.number()]); // [number, number]z.record适合键不固定但值类型统一的场景。在 ui/actions/users/users.ts 中过滤条件就是一个典型的 recordfilters: z .record( z.string(), z.union([z.string(), z.array(z.string()), z.number()]).optional(), ) .default({}),注意 Zod 4 中z.array(...).nonempty()仍然可用等价于.min(1)但若追求与z.string()一致的风格建议统一使用.min(1)。联合与可辨识联合// Simple union简单联合 const stringOrNumber z.union([z.string(), z.number()]); // Discriminated union可辨识联合更高效 const resultSchema z.discriminatedUnion(status, [ z.object({ status: z.literal(success), data: z.unknown() }), z.object({ status: z.literal(error), error: z.string() }), ]);可辨识联合discriminated union要求每个成员对象都有同一个literal判别字段此处为statusZod 据此做快速分支匹配性能优于逐个尝试的z.union。Prowler UI 在 Lighthouse 配置模块中贯彻了这一思想先定义各 Provider 的凭据变体类型config.ts/lighthouse/_lib/config.ts#L185-L208) 中的buildLighthouseV2ConfigurationInput用switch把动态的provider收窄为具体的联合成员再从表单数据构造可辨识输入对象。这种先由 superRefine 校验字段再在单一入口收窄类型的做法把类型断言限制在一个函数内保证所有调用方获得完整的联合类型检查。转换与预处理// Transform during parsing解析期转换 const lowercaseEmail z.email().transform(email email.toLowerCase()); // Coercion类型强转 const numberFromString z.coerce.number(); // 42 → 42 const dateFromString z.coerce.date(); // 2024-01-01 → Date // Preprocessing解析前预处理 const trimmedString z.preprocess( val typeof val string ? val.trim() : val, z.string() );z.coerce在 Prowler UI 的 Server Actions 中承担了关键职责表单提交的FormData值都是字符串必须转成数字。上述getUsersSchema中的z.coerce.number().int().min(1)即为一例——它同时完成了字符串→数字转换与范围校验。而 ui/types/authFormSchema.ts 展示了 transform 链的另一种用法把邮箱归一化const baseAuthSchema z.object({ email: z .email({ message: Please enter a valid email address. }) .trim() .toLowerCase(), password: z.string(), isSamlMode: z.boolean().optional(), });Zod 4 在字符串类型上内置了trim()、toLowerCase()、toUpperCase()等便捷方法配合z.email()顶层校验器无需再手写z.preprocess。精细化校验refine 与 superRefine当内置校验器无法表达业务规则时用refine追加条件校验const passwordSchema z.string() .min(8) .refine(val /[A-Z]/.test(val), { message: Must contain uppercase letter, }) .refine(val /[0-9]/.test(val), { message: Must contain number, }); // 跨字段校验用 superRefine可累积多个错误 const formSchema z.object({ password: z.string(), confirmPassword: z.string(), }).superRefine((data, ctx) { if (data.password ! data.confirmPassword) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: Passwords dont match, path: [confirmPassword], }); } });两者的分工是refine只返回布尔值适合单字段的简单条件superRefine通过ctx.addIssue精确控制错误码、消息与path支持在一个回调里累积多个错误。仓库中有两个教科书级的 superRefine 案例案例一登录表单的 SAML 条件校验ui/types/authFormSchema.ts。登录时若处于 SAML 模式则密码可为空否则必填——这种跨字段条件逻辑用refinepath指向具体字段实现export const signInSchema baseAuthSchema .extend({ password: z.string() }) .refine( (data) { if (data.isSamlMode) return true; // SAML 模式不要求密码 return data.password.length 0; // 否则密码必填 }, { message: Password is required., path: [password] }, );案例二Lighthouse LLM Provider 配置的按 Provider 差异化校验config.ts/lighthouse/_lib/config.ts#L73-L143)。同一个 Schema 依据运行时的provider值决定校验哪些字段OpenAI 兼容 Provider 要求baseUrl合法 URL用new URL(value)兜底校验见 isValidUrl/lighthouse/_lib/config.ts#L171-L178)Bedrock 则要求 AWS 三件套凭证齐全。所有错误都通过ctx.addIssue定位到具体path使表单错误可以精确渲染到对应输入框。案例三扫描配置的 YAML 语法预检formSchemas.ts。服务端safeParse前先用superRefine调用validateYaml校验 YAML 语法使格式错误以表单错误的形式提前暴露而不是在后端返回 500export const scanConfigurationFormSchema z.object({ name: z.string().trim().min(3, { message: ... }).max(100, { message: ... }), configuration: z .string() .trim() .min(1, { error: Configuration is required }) .superRefine((val, ctx) { const yamlValidation validateYaml(val); if (!yamlValidation.isValid) { ctx.addIssue({ code: custom, message: Invalid YAML format: ${yamlValidation.error}, }); } }), provider_ids: z.array(z.uuid()).optional().default([]), });Optional、Nullable 与默认值// OptionalT | undefined z.string().optional() // NullableT | null z.string().nullable() // 两者兼有T | null | undefined z.string().nullish() // 默认值支持惰性求值 z.string().default(unknown) z.number().default(() Math.random())使用建议optional()适用于字段可缺失nullable()适用于字段可为 null如 API 响应default()则让解析结果永远有值避免调用方反复判空。Prowler UI 在 Alert 表单中大量使用default([])、default()见 alert-form-schema.ts/alerts/_lib/alert-form-schema.ts#L15-L21)保证parse输出的对象结构稳定在查询参数 Schema 中则用z.coerce.number().int().min(1).default(1)提供分页默认值。错误处理error 参数与自定义错误映射Zod 4 的核心变化是用error参数统一错误信息// Zod 4: 使用 error 参数替代 message const schema z.object({ name: z.string({ error: Name must be a string }), email: z.email({ error: Invalid email format }), age: z.number().min(18, { error: Must be 18 or older }), }); // 自定义错误映射根据 issue.code 返回不同文案 const customSchema z.string({ error: (issue) { if (issue.code too_small) { return String is too short; } return Invalid string; }, });error参数既可以是字符串也可以是接收ZodIssue返回文案的函数从而实现按错误码定制提示语。解析失败后用result.error.issues或flatten()提取结构化错误信息。Prowler UI 的 Server Actions 展示了一套完整的错误处理流水线safeParseflatten()在 ui/actions/scan-configurations/scan-configurations.ts 中校验失败后调用validated.error.flatten().fieldErrors按字段名取出首条错误并映射为表单错误状态。safeParse 提前返回在 ui/actions/users/users.ts 中getUsers校验失败直接console.error并返回undefined避免把非法参数拼进请求 URLupdateUser则返回{ error: Invalid user data }供 UI 展示。服务端错误路由回字段scan-configurations.ts中的mapApiErrorsToFields根据 JSON:API 错误的source.pointer把后端错误分别路由到name、configuration、provider_ids字段ui/actions/scan-configurations/scan-configurations.ts保证创建与更新两条流程呈现一致的错误位置。此外UUID 校验被用作安全边界。扫描配置模块在把 ID 拼入 URL 前先做z.uuid()校验ui/actions/scan-configurations/scan-configurations.ts防止畸形或伪造的 ID 注入路径段——这是把 Zod 校验从表单体验升级为安全防线的典型实践。React Hook Form 集成Zod 4 与 React Hook Form 通过hookform/resolvers/zod的zodResolver无缝衔接import { useForm } from react-hook-form; import { zodResolver } from hookform/resolvers/zod; const schema z.object({ email: z.email(), password: z.string().min(8), }); type FormData z.infertypeof schema; function Form() { const { register, handleSubmit, formState: { errors } } useFormFormData({ resolver: zodResolver(schema), }); return ( form onSubmit{handleSubmit(onSubmit)} input {...register(email)} / {errors.email span{errors.email.message}/span} /form ); }在 ui/components/auth/oss/sign-in-form.tsx 中可以看到生产级用法useFormSignInFormData({ resolver: zodResolver(signInSchema), mode: onSubmit, reValidateMode: onSubmit, ... })——mode: onSubmit控制首次校验时机为提交时避免输入过程中的打扰式报错。对于参数随选择动态变化的场景query-builder/_hooks/use-query-builder.ts/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts#L18-L43) 演示了动态构建 Schema 的手法根据攻击路径查询的参数元数据param.data_type在运行时为每个字段生成对应的 Zod 校验器number用z.coerce.number().refine(val val 0)boolean用z.boolean().default(false)再统一z.object(schemaObject)组装let fieldSchema: z.ZodTypeAny isCustomQueryParameter ? customAttackPathQuerySchema : z.string().min(1, ${param.label} is required); if (param.data_type number) { fieldSchema z.coerce.number().refine((val) val 0, { message: ${param.label} must be a non-negative number, }); } else if (param.data_type boolean) { fieldSchema z.boolean().default(false); } schemaObject[param.name] fieldSchema; // ... return z.object(schemaObject);然后通过zodResolver(getValidationSchema(selectedQueryData))挂到useForm上use-query-builder.ts/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts#L66-L71)切换查询时用form.reset(getDefaultValues(...))重建默认值——动态 Schema 动态默认值的组合解决了通用表单引擎的核心难题。仓库中的 Zod 4 落地全景从源码统计可以看出Zod 4 已经渗透到 Prowler UI 的各个层级以下均为当前仓库可验证的路径服务端 Actions 层用户管理ui/actions/users/users.ts、扫描配置ui/actions/scan-configurations/scan-configurations.ts、攻击路径ui/actions/attack-paths/、合规关注列表ui/actions/overview/compliance-watchlist/等统一用safeParse在 Action 边界校验FormData与查询参数。表单 Schema 集中定义认证表单ui/types/authFormSchema.ts、各类表单 Schemaui/types/formSchemas.ts以及按模块就近定义的 alert-form-schema.ts/alerts/_lib/alert-form-schema.ts)。React Hook Form 集成登录/注册sign-in-form.tsx、集成配置表单ui/components/integrations/、邀请管理ui/components/invitations/、Lighthouse 配置configuration-form.tsx/lighthouse/_components/config/configuration-form.tsx)等组件均使用zodResolver。动态 Schema 构建攻击路径查询构建器use-query-builder.ts/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts)。这种集中定义 Schema 服务端/客户端双侧复用的架构让同一份校验规则同时服务于表单 UX 与请求安全是 Zod 4 在现代全栈应用中的推荐组织方式。迁移检查清单最后把 v3 → v4 迁移的要点浓缩为可勾选的清单将z.string().email() / .uuid() / .url()替换为顶层z.email() / z.uuid() / z.url()将z.string().nonempty()替换为z.string().min(1)将required_error等专用参数改为第二个参数{ error: ... }基础校验器string/min/max/email等的错误文案统一用errorrefine/superRefine回调内仍用message对来自FormData或 URL 的字符串数字使用z.coerce.number()避免手写Number() 判空用z.infer推导输出类型、z.input表示输入类型配合safeParse的flatten()产出结构化错误跨字段或条件校验优先使用superRefinectx.addIssue指定code、message、path在把用户输入如 ID拼进请求 URL 前用z.uuid()等校验器做安全边界检查与 React Hook Form 集成时使用zodResolver并根据交互需要配置mode/reValidateMode按照这份清单逐项检查即可把既有 Zod 3 代码库平稳迁移到 Zod 4并获得类型推断、错误结构与性能上的全面升级。【免费下载链接】prowlerProwler is the world’s most widely used open-source cloud security platform that automates security and compliance across any cloud environment.项目地址: https://gitcode.com/GitHub_Trending/pr/prowler创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考