You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 04:35:58