Skip to content

Commit

Permalink
Improve Zod error messages (#1320)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrpospiech committed May 24, 2024
1 parent 75dbb94 commit a77b8e2
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 8 deletions.
9 changes: 6 additions & 3 deletions packages/uniforms-bridge-zod/__tests__/ZodBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,23 +132,26 @@ describe('ZodBridge', () => {
const schema = object({ a: string(), b: number() });
const bridge = new ZodBridge({ schema });
const error = bridge.getValidator()({});
const messages = error?.issues?.map(issue => issue.message);
const messages = ['A: Required', 'B: Required'];
expect(bridge.getErrorMessages(error)).toEqual(messages);
});

it('works with arrays', () => {
const schema = object({ a: array(array(string())) });
const bridge = new ZodBridge({ schema });
const error = bridge.getValidator()({ a: [['x', 'y', 0], [1]] });
const messages = error?.issues?.map(issue => issue.message);
const messages = [
'A (0, 2): Expected string, received number',
'A (1, 0): Expected string, received number',
];
expect(bridge.getErrorMessages(error)).toEqual(messages);
});

it('works with nested objects', () => {
const schema = object({ a: object({ b: object({ c: string() }) }) });
const bridge = new ZodBridge({ schema });
const error = bridge.getValidator()({ a: { b: { c: 1 } } });
const messages = error?.issues?.map(issue => issue.message);
const messages = ['C: Expected string, received number'];
expect(bridge.getErrorMessages(error)).toEqual(messages);
});
});
Expand Down
27 changes: 23 additions & 4 deletions packages/uniforms-bridge-zod/src/ZodBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ZodDefault,
ZodEnum,
ZodError,
ZodIssue,
ZodNativeEnum,
ZodNumber,
ZodNumberDef,
Expand All @@ -28,6 +29,23 @@ function isNativeEnumValue(value: unknown) {
return typeof value !== 'string';
}

function getLabel(value: unknown) {
return upperFirst(lowerCase(joinName(null, value).slice(-1)[0]));
}

function getFullLabel(path: ZodIssue['path'], indexes: number[] = []): string {
const lastElement = path[path.length - 1];

if (typeof lastElement === 'number') {
const slicedPath = path.slice(0, path.length - 1);
return getFullLabel(slicedPath, [lastElement, ...indexes]);
}

return indexes.length > 0
? `${getLabel(path)} (${indexes.join(', ')})`
: getLabel(path);
}

/** Option type used in SelectField or RadioField */
type Option<Value> = {
disabled?: boolean;
Expand Down Expand Up @@ -73,9 +91,10 @@ export default class ZodBridge<T extends ZodRawShape> extends Bridge {

getErrorMessages(error: unknown) {
if (error instanceof ZodError) {
// TODO: There's no information which field caused which error. We could
// do some generic prefixing, e.g., `{name}: {message}`.
return error.issues.map(issue => issue.message);
return error.issues.map(issue => {
const name = getFullLabel(issue.path);
return `${name}: ${issue.message}`;
});
}

if (error instanceof Error) {
Expand Down Expand Up @@ -149,7 +168,7 @@ export default class ZodBridge<T extends ZodRawShape> extends Bridge {
getProps(name: string) {
const props: UnknownObject & { options?: Option<unknown>[] } = {
...(this.provideDefaultLabelFromFieldName && {
label: upperFirst(lowerCase(joinName(null, name).slice(-1)[0])),
label: getLabel(name),
}),
required: true,
};
Expand Down
4 changes: 3 additions & 1 deletion packages/uniforms/__suites__/ErrorsField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export function testErrorsField(ErrorsField: ComponentType<any>) {
]),
schema: z.object({ x: z.boolean(), y: z.number(), z: z.string() }),
});
expect(screen.getAllByText(errorMessage)).toHaveLength(3);
expect(screen.getByText(`X: ${errorMessage}`)).toBeInTheDocument();
expect(screen.getByText(`Y: ${errorMessage}`)).toBeInTheDocument();
expect(screen.getByText(`Z: ${errorMessage}`)).toBeInTheDocument();
});

test('<ErrorsField> - renders error from props', () => {
Expand Down

0 comments on commit a77b8e2

Please sign in to comment.