sveltekit-superforms icon indicating copy to clipboard operation
sveltekit-superforms copied to clipboard

SchemaError with discriminated unions, default values and superRefine

Open sproott opened this issue 5 months ago • 10 comments

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 superRefine which 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:

Image

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);

sproott avatar Jun 10 '25 14:06 sproott

I managed to reproduce this bug even with the default value matching the type: repro

sproott avatar Jun 11 '25 09:06 sproott

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?

ciscoheat avatar Jun 16 '25 12:06 ciscoheat

2.27.0 released, please test if it works there.

ciscoheat avatar Jun 16 '25 20:06 ciscoheat

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.

sproott avatar Jun 18 '25 09:06 sproott

The first one is more complicated, need to spend some time on that.

ciscoheat avatar Jun 19 '25 06:06 ciscoheat

Should be fixed now in 2.27.1

ciscoheat avatar Jun 27 '25 13:06 ciscoheat

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?

sproott avatar Jun 29 '25 12:06 sproott

Also, what's the reasoning behind setting these defaults automatically in the presence of an error?

Can this behavior be somehow disabled?

sproott avatar Jun 29 '25 12:06 sproott

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.

ciscoheat avatar Jun 29 '25 17:06 ciscoheat

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.

sproott avatar Jul 08 '25 07:07 sproott