sveltekit-superforms
sveltekit-superforms copied to clipboard
SchemaError with discriminated unions, default values and superRefine
Error trace:
Error: [additional.name] No types found for defaults
❯ Defaults_traverseAndReplace src/lib/errors.ts:293:11
291| // Return if checking for errors, as there may be deep errors that doesn't exist in the defaults.
292| if (traversingErrors) return;
293| throw new SchemaError('No types found for defaults', currentPath);
| ^
294| }
295|
❯ traversePaths src/lib/traversal.ts:107:18
❯ traversePaths src/lib/traversal.ts:112:19
❯ Data_traverse src/lib/errors.ts:220:3
❯ replaceInvalidDefaults src/lib/errors.ts:306:10
❯ superValidate src/lib/superValidate.ts:144:5
❯ validate src/tests/zodUnion.test.ts:17:10
❯ src/tests/zodUnion.test.ts:153:3
Description I have a big form schema which contains a nested discriminated union. I'm setting default values on many fields which don't match the type.
I've encountered the error only when these conditions are met:
- Schema contains a nested discriminated union
- One of its fields has a default value which does not match its output type
- The schema is wrapped in a
superRefinewhich adds an issue when run
Note: it seemed weird to me that in the Defaults_traverseAndReplace function, when printing the Types object, the information stops at the first discriminated union, having only __types: ['object'] but no other fields.
I've seen this note in the docs, but I'm not sure what to make of it:
Is this error expected? If so, are there any workarounds to make it work?
If applicable, a MRE Reproduction here
Code
const ZodSchema2 = z
.discriminatedUnion('type', [
z.object({
type: z.literal('empty')
}),
z.object({
type: z.literal('additional'),
additional: z.discriminatedUnion('type', [
z.object({
type: z.literal('poBox'),
name: z
.string()
.min(1, 'min len')
.max(10, 'max len')
.default(null as unknown as string)
}),
z.object({
type: z.literal('none')
})
])
})
])
.superRefine((_data, ctx) => {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['addresses', 'additional', 'name'],
message: 'error'
});
});
const FormSchema = zod(ZodSchema2);
type FormSchema = (typeof FormSchema)['defaults'];
const data = {
type: 'additional',
additional: {
type: 'poBox',
name: ''
}
} satisfies FormSchema;
await validate(data, FormSchema);
I managed to reproduce this bug even with the default value matching the type: repro
I pasted the test into the file, but it passed. Also, in the superRefine call I see the path ['addresses', 'additional', 'name'] but no addresses in the schema. I'll release a new version soon, can you test it then?
2.27.0 released, please test if it works there.
Thanks for the update. Can confirm that the second test with the matching type now passes, but the first one (with the default value not matching the type) doesn't. Also, correcting the path in the superRefine doesn't fix it either.
The first one is more complicated, need to spend some time on that.
Should be fixed now in 2.27.1
Thanks, the original error is gone now.
I've just noticed that now the validation changes valid form values unexpectedly (again in the presence of a nested discriminated union). The reproduction is here.
Shall I move this into a new issue, or is it fine here?
Also, what's the reasoning behind setting these defaults automatically in the presence of an error?
Can this behavior be somehow disabled?
The runtime type checking for invalid values is complicated and it's not easier with multiple nested discriminated unions. After merging the union type data, the path traversal didn't stop going down after a valid data match. It will be fixed in the next release.
I've found another bug that reproduces even with the latest commit. This happens when the union branch has a default value that's essentially invalid (as recommended in the docs), e.g. a number field that's null by default, to force the user to fill it in. This also happens without using superRefine, as the default value itself is invalid, and raises a validation error, which sveltekit-superforms resolves incorrectly by replacing the union with a different branch. Here's the repro.