Skip to content

Commit

Permalink
fix(validator-zod): Surface nested errors when validating a union type (
Browse files Browse the repository at this point in the history
#294)

* feat(validator-zod): add failing zod union type validation test

* fix(validator-zod): validator now surfaces union type errors (#248)

* chore(validator-zod): add changeset

* feat: add zod discriminatedUnion validation test
  • Loading branch information
sea-grass authored Nov 1, 2024
1 parent 01566f1 commit 5d90c05
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-trees-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@felte/validator-zod': minor
---

Fixes an issue where errors from validating Zod union types weren't captured (#248)
55 changes: 31 additions & 24 deletions packages/validator-zod/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Data extends Obj = Obj> = {
schema: ZodSchema<Data>;
level?: 'error' | 'warning';
};

export function validateSchema<Data extends Obj>(
schema: ZodSchema
): ValidationFunction<Data> {
function shapeErrors(errors: ZodError): AssignableErrors<Data> {
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<Data>
): AssignableErrors<Data> {
for (const issue of error.issues) {
if (issue.code === 'invalid_union') {
for (const unionError of issue.unionErrors) {
err = walk(unionError, err);
}
);
}, {} as AssignableErrors<Data>);
} 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<AssignableErrors<Data> | undefined> {
try {
await schema.parseAsync(values);
} catch (error) {
return shapeErrors(error as ZodError<any>);
const result = await schema.safeParseAsync(values);
if (!result.success) {
let err = {} as AssignableErrors<Data>;
err = walk(result.error, err);
return err;
}
};
}
Expand Down
43 changes: 41 additions & 2 deletions packages/validator-zod/tests/validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -312,7 +312,7 @@ describe('Validator zod', () => {
});
const mockData = { elems: [null, { name: '' }] };

const { validate, errors } = createForm<typeof mockData>({
const { validate, errors } = createForm({
initialValues: mockData,
onSubmit: vi.fn(),
extend: validator({ schema }),
Expand All @@ -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'] });
});
});

0 comments on commit 5d90c05

Please sign in to comment.