diff --git a/.changeset/loud-trees-approve.md b/.changeset/loud-trees-approve.md new file mode 100644 index 00000000..8c98dfcd --- /dev/null +++ b/.changeset/loud-trees-approve.md @@ -0,0 +1,5 @@ +--- +'@felte/validator-zod': minor +--- + +Fixes an issue where errors from validating Zod union types weren't captured (#248) diff --git a/packages/validator-zod/src/index.ts b/packages/validator-zod/src/index.ts index a236cf2b..8b236f19 100644 --- a/packages/validator-zod/src/index.ts +++ b/packages/validator-zod/src/index.ts @@ -7,42 +7,49 @@ import type { Extender, } from '@felte/common'; import { _update } from '@felte/common'; -import type { ZodError, AnyZodObject } from 'zod'; +import type { ZodError, ZodSchema } from 'zod'; -type ZodSchema = { - parseAsync: AnyZodObject['parseAsync']; -}; - -export type ValidatorConfig = { - schema: ZodSchema; +export type ValidatorConfig = { + schema: ZodSchema; level?: 'error' | 'warning'; }; export function validateSchema( schema: ZodSchema ): ValidationFunction { - function shapeErrors(errors: ZodError): AssignableErrors { - return errors.issues.reduce((err, value) => { - /* istanbul ignore next */ - if (!value.path) return err; - return _update( - err, - value.path.join('.'), - (currentValue: undefined | string[]) => { - if (!currentValue || !Array.isArray(currentValue)) - return [value.message]; - return [...currentValue, value.message]; + function walk( + error: ZodError, + err: AssignableErrors + ): AssignableErrors { + for (const issue of error.issues) { + if (issue.code === 'invalid_union') { + for (const unionError of issue.unionErrors) { + err = walk(unionError, err); } - ); - }, {} as AssignableErrors); + } else { + if (!issue.path) continue; + + const updater = (currentValue?: string[]) => { + if (!currentValue || !Array.isArray(currentValue)) { + return [issue.message]; + } + return [...currentValue, issue.message]; + }; + err = _update(err, issue.path.join('.'), updater); + } + } + + return err; } + return async function validate( values: Data ): Promise | undefined> { - try { - await schema.parseAsync(values); - } catch (error) { - return shapeErrors(error as ZodError); + const result = await schema.safeParseAsync(values); + if (!result.success) { + let err = {} as AssignableErrors; + err = walk(result.error, err); + return err; } }; } diff --git a/packages/validator-zod/tests/validator.spec.ts b/packages/validator-zod/tests/validator.spec.ts index 7dcf4771..daa7d3e2 100644 --- a/packages/validator-zod/tests/validator.spec.ts +++ b/packages/validator-zod/tests/validator.spec.ts @@ -2,7 +2,7 @@ import '@testing-library/jest-dom/vitest'; import { expect, describe, test, vi } from 'vitest'; import { createForm } from './common'; import { validateSchema, validator } from '../src'; -import { z } from 'zod'; +import { z, ZodSchema } from 'zod'; import { get } from 'svelte/store'; type Data = { @@ -312,7 +312,7 @@ describe('Validator zod', () => { }); const mockData = { elems: [null, { name: '' }] }; - const { validate, errors } = createForm({ + const { validate, errors } = createForm({ initialValues: mockData, onSubmit: vi.fn(), extend: validator({ schema }), @@ -324,4 +324,43 @@ describe('Validator zod', () => { elems: [{ 0: null }, { name: null }], }); }); + + test('should surface union type errors', async () => { + async function t(schema: ZodSchema, initialValues: object) { + const { validate, errors } = createForm({ + initialValues, + extend: validator({ schema }), + }); + await validate(); + return get(errors); + } + + const schema = z.object({ foo: z.string().min(1) }); + const data = { foo: '' }; + + const unionErrors = await t(z.union([schema, schema]), data); + const errors = await t(schema, data); + + expect(unionErrors).to.deep.equal(errors); + }); + + test('should surface discriminatedUnion type errors', async () => { + async function t(schema: ZodSchema, initialValues: object) { + const { validate, errors } = createForm({ + initialValues, + extend: validator({ schema }), + }); + await validate(); + return get(errors); + } + + const schema = z.discriminatedUnion('type', [ + z.object({ type: z.literal('foo'), foo: z.string().min(1) }), + z.object({ type: z.literal('bar'), bar: z.string().min(1) }) + ], { errorMap: () => ({ message: 'Oops' }) }); + + const errors = await t(schema, { type: 'baz' }); + + expect(errors).to.deep.equal({ type: ['Oops'] }); + }); });