When using useForm with toTypedSchema, a nested object field that is not present in initialValues gets initialized differently depending on whether the root schema is a plain ZodObject or a ZodEffects (produced by .refine() / .superRefine()).
- Root schema is
ZodObject (no .refine()) → field is initialized as { value: undefined, label: undefined }
- Root schema is
ZodEffects (with .refine()) → field is initialized as undefined
This inconsistency is surprising because the shape of the nested field itself has not changed — only the root schema wrapper differs.
Minimal Reproduction
const schema = z.object({
name: z.string(),
priceType: z.object({
value: z.string(),
label: z.string(),
}),
});
// Without .refine() — root is ZodObject
const { values } = useForm({
validationSchema: toTypedSchema(schema),
initialValues: { name: '' },
});
console.log(values.priceType);
// → { value: undefined, label: undefined }
// With .refine() — root is ZodEffects
const { values: values2 } = useForm({
validationSchema: toTypedSchema(schema.refine(() => true)),
initialValues: { name: '' },
});
console.log(values2.priceType);
// → undefined
Observed Cause
The cast function calls getDefaults(zodSchema) when zodSchema.parse() throws. getDefaults checks instanceof ZodObject at the top level:
function getDefaults(schema) {
if (!(schema instanceof ZodObject)) {
return undefined; // ZodEffects reaches this and returns early
}
return Object.fromEntries(Object.entries(schema.shape).map(([key, value]) => {
// ...
if (value instanceof ZodObject) {
return [key, getDefaults(value)]; // produces { value: undefined, label: undefined }
}
return [key, undefined];
}));
}
When the root is ZodEffects, getDefaults returns undefined immediately, so no synthetic defaults are injected and the nested field stays undefined. When the root is ZodObject, getDefaults recurses into nested ZodObject fields and synthesizes an empty object for them.
Workaround
Explicitly set the field to undefined in initialValues:
const { values } = useForm({
validationSchema: toTypedSchema(schema),
initialValues: {
name: '',
priceType: undefined,
},
});
Question
Is it intended that nested object fields without an explicit .default() are synthesized as { field: undefined } rather than left as undefined? And if so, should the behavior be consistent regardless of whether the root schema is ZodObject or ZodEffects?
When using
useFormwithtoTypedSchema, a nested object field that is not present ininitialValuesgets initialized differently depending on whether the root schema is a plainZodObjector aZodEffects(produced by.refine()/.superRefine()).ZodObject(no.refine()) → field is initialized as{ value: undefined, label: undefined }ZodEffects(with.refine()) → field is initialized asundefinedThis inconsistency is surprising because the shape of the nested field itself has not changed — only the root schema wrapper differs.
Minimal Reproduction
Observed Cause
The
castfunction callsgetDefaults(zodSchema)whenzodSchema.parse()throws.getDefaultschecksinstanceof ZodObjectat the top level:When the root is
ZodEffects,getDefaultsreturnsundefinedimmediately, so no synthetic defaults are injected and the nested field staysundefined. When the root isZodObject,getDefaultsrecurses into nestedZodObjectfields and synthesizes an empty object for them.Workaround
Explicitly set the field to
undefinedininitialValues:Question
Is it intended that nested object fields without an explicit
.default()are synthesized as{ field: undefined }rather than left asundefined? And if so, should the behavior be consistent regardless of whether the root schema isZodObjectorZodEffects?