knightkerchief
knightkerchief

Reputation: 17

React Hook Form Treating Optional Fields as Required When Using Zod Resolver

I’m using React Hook Form with Zod for form validation. My schema has optional fields using .partial(), and when I manually validate the schema with safeParse(), it correctly treats the fields as optional. However, React Hook Form still considers them required and showing validation errors when they are left empty.

Key Observations:

Our Use Case:

Codesandbox

Here is the schema code:

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({
    // email: true,
    // password: true,
    first_name: true,
    last_name: true,
    phone: true,
  });
const data = authSchema.safeParse({
  email: "[email protected]",
  password: "123456",
});
console.log("IsSuccess::", data.success); // true

const allOptional = authSchema;

type AuthForm = z.infer<typeof allOptional>;

Here is the component code

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>
  );
}

Upvotes: 0

Views: 51

Answers (2)

vzsoares
vzsoares

Reputation: 911

This is a known issue e.g. And it comes from the fact that html inputs use a empty string as default value. source

When using optional in zod you're saying that the field can be undefined which is different from empty string "". That causes zod to validate it against the z.string schema and not the z.optional.

The solution to this as stated in the zod docs is to use a union and accept the literal value of "".

Now it's just a matter adding that to the schemas you want to be optional. There's many ways in achieving this. But here is a reusable solution:

const fixHtmlFormOptionalFields = <T extends z.ZodObject<any, any>>(
  schema: T
): T => {
  const entries = Object.entries(schema.shape);

  const transformedEntries = entries.map(([key, value]) => {
    // Only transform optional schemas
    if (value instanceof z.ZodOptional) {
      return [key, z.union([value, z.literal("")])];
    }

    return [key, value];
  });

  return z.object(Object.fromEntries(transformedEntries)) as T;
};

// use this one now
const fixedSchema = fixHtmlFormOptionalFields(authSchema);

sandbox

Upvotes: 1

emifervi
emifervi

Reputation: 1

Heyo, I had the same thing going on earlier today. After I did some more googling, I found that devklepacki gave a good solution in this forum post. It has been working for me, hope it works for you as well!!!

Upvotes: 0

Related Questions