Reputation: 17
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:
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
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);
Upvotes: 1