`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 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`?
0 条评论