`toTypedSchema` initializes nested object fields as `{field: undefined}` instead of `undefined` when root schema is `ZodObject`, but not when it is `ZodEffects`
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 asundefined
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);
// → undefinedObserved 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?
Source: logaretm/vee-validate