使用Zod Resolver时React Hook Form误将可选字段视为必填
React Hook Form 结合 Zod 校验时,
.partial() 标记的可选字段仍被视为必填 我用 React Hook Form 搭配 Zod 做表单校验,已经通过 .partial() 把部分字段设为可选,手动调用 safeParse() 校验时 Zod 能正确识别这些字段可以为空,但 React Hook Form 还是把它们当成必填项,留空就弹出校验错误。
关键观察
- Zod 本身没问题:执行
safeParse({ email: "test@test.com", password: "123456" })后日志输出IsSuccess:: true,确认 Zod 允许那些可选字段缺省。
使用场景
- 维护一个包含所有表单字段的基础 Schema 库;
- 根据不同表单的需求,从基础库中挑选需要的字段;
- 动态将选中的部分字段设置为可选。
Schema 代码
const baseSchema = z.object({ email: z .string() .nonempty({ message: "Email is required" }) .email({ message: "Invalid email address" }), password: z .string() .nonempty({ message: "Password is required" }) .min(6, { message: "Password should be min 6 characters" }), first_name: z .string() .min(1, { message: "First name is required" }) .max(34, { message: "First name can only have 34 characters " }), last_name: z.string().min(1, { message: "Last name is required" }), phone: z .string() .max(16, { message: "Phone number can only be 16 characters long" }) .regex(/^( )*(0|\+)(\+|0?\d)([0-9]| |[-()])*$/, { message: "Phone number in not correct", }), street: z.string().min(1, { message: "Street is required" }), house_number: z.string().min(1, { message: "House number is required" }), }); const authSchema = baseSchema .pick({ email: true, password: true, first_name: true, last_name: true, phone: true, }) .partial({ first_name: true, last_name: true, phone: true, }); const data = authSchema.safeParse({ email: "test@test.com", password: "123456", }); console.log("IsSuccess::", data.success); // 输出 true const allOptional = authSchema; type AuthForm = z.infer<typeof allOptional>;
组件代码
export default function App() { const { register, handleSubmit, formState: { errors }, } = useForm<AuthForm>({ resolver: zodResolver(allOptional), mode: "onBlur", }); const onSubmit: SubmitHandler<AuthForm> = useCallback(async (value) => { console.log(value); }, []); return ( <form onSubmit={handleSubmit(onSubmit)} className=""> <label> <span>First name</span> <input {...register("first_name")} /> <span>{errors.first_name?.message}</span> </label> <label> <span>Last name</span> <input {...register("last_name")} /> <span>{errors.last_name?.message}</span> </label> <label> <span>Phone</span> <input {...register("phone")} /> <span>{errors.phone?.message}</span> </label> <label> <span>Email</span> <input {...register("email")} /> <span>{errors.email?.message}</span> </label> <label> <span>Password</span> <input {...register("password")} /> <span>{errors.password?.message}</span> </label> <button type="submit">Submit</button> </form> ); }
问题原因及解决方法
问题根源在于:Zod 的 .partial() 只是把字段标记为「可选(可以不存在)」,但字段本身的校验规则(比如 min(1)、nonempty())依然生效。当 React Hook Form 处理表单时,输入框的空字符串会被提交给 Zod,而不是 undefined,这时候 Zod 会执行字段内部的校验规则,导致报错——因为空字符串不符合 min(1) 或 nonempty() 的要求。
三种可行的解决方式
1. 修改 Schema,让可选字段允许空字符串
直接在 Schema 中重新定义可选字段的规则,允许空字符串或者 undefined:
const authSchema = baseSchema .pick({ email: true, password: true, first_name: true, last_name: true, phone: true, }) .partial({ first_name: true, last_name: true, phone: true, }) // 覆盖可选字段的规则:允许空字符串或符合原校验规则 .extend({ first_name: z.string().max(34, { message: "First name can only have 34 characters " }).optional().or(z.literal("")), last_name: z.string().optional().or(z.literal("")), phone: z.string() .max(16, { message: "Phone number can only be 16 characters long" }) .regex(/^( )*(0|\+)(\+|0?\d)([0-9]| |[-()])*$/, { message: "Phone number in not correct", }).optional().or(z.literal("")), });
2. 在 React Hook Form 注册字段时转换空值
注册字段时,把空字符串转换成 undefined,这样 Zod 会把它当成未提交的字段,触发 .partial() 的规则:
<input {...register("first_name", { setValueAs: (value) => value.trim() || undefined, })} />
对所有需要可选的字段都应用这个转换逻辑即可。
3. 使用 Zod 的 .superRefine() 自定义校验逻辑
如果不想修改原 Schema 的结构,可以用 .superRefine() 手动处理可选字段的校验:
const authSchema = baseSchema .pick({ email: true, password: true, first_name: true, last_name: true, phone: true, }) .partial({ first_name: true, last_name: true, phone: true, }) .superRefine((data, ctx) => { // 处理 first_name:如果是空字符串则跳过校验,否则检查长度 if (data.first_name !== undefined) { if (data.first_name.trim() === "") { return; } if (data.first_name.length < 1) { ctx.addIssue({ code: z.ZodIssueCode.too_small, minimum: 1, path: ["first_name"], message: "First name is required", }); } } // 同理处理 last_name 和 phone });
内容的提问来源于stack exchange,提问作者knightkerchief
相关产品推荐
相关产品推荐

