Skip to content

Commit

Permalink
Include received value type in invalid_union_discriminator issues
Browse files Browse the repository at this point in the history
  • Loading branch information
lo1tuma committed Mar 28, 2024
1 parent 610f670 commit f2e5443
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 52 deletions.
17 changes: 15 additions & 2 deletions integration-tests/zod-error-formatter/discriminated-union.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test('formats messages for invalid discriminated union schemas correctly', () =>

assert.strictEqual(result.success, false);
assert.deepStrictEqual(result.error.issues, [
'at type: invalid discriminator value, expected one of "foo" or "bar"'
'at type: invalid discriminator: expected one of "foo" or "bar", but got number'
]);
});

Expand All @@ -25,6 +25,19 @@ test('formats messages for invalid discriminated union schemas correctly when th

assert.strictEqual(result.success, false);
assert.deepStrictEqual(result.error.issues, [
'at type: invalid discriminator value, expected one of "foo" or "bar"'
'at type: missing property'
]);
});

test('formats messages for invalid discriminated union schemas correctly when discriminator is undefined', () => {
const schema = z.discriminatedUnion('type', [
z.object({ type: z.literal('foo'), data: z.number() }),
z.object({ type: z.literal('bar'), data: z.boolean() })
]);
const result = safeParse(schema, { type: undefined });

assert.strictEqual(result.success, false);
assert.deepStrictEqual(result.error.issues, [
'at type: invalid discriminator: expected one of "foo" or "bar", but got undefined'
]);
});
38 changes: 19 additions & 19 deletions source/zod-error-formatter/format-issue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { ZodError } from 'zod';
import { formatIssue } from './format-issue.js';

test('returns just the message when the path is empty', () => {
const formattedIssue = formatIssue({ code: 'custom', message: 'foo', path: [] });
const formattedIssue = formatIssue({ code: 'custom', message: 'foo', path: [] }, '');
assert.strictEqual(formattedIssue, 'invalid input');
});

test('returns the message with path when the path is not empty', () => {
const formattedIssue = formatIssue({ code: 'custom', message: 'bar', path: ['foo'] });
const formattedIssue = formatIssue({ code: 'custom', message: 'bar', path: ['foo'] }, '');
assert.strictEqual(formattedIssue, 'at foo: invalid input');
});

Expand All @@ -20,7 +20,7 @@ test('returns the formatted issue when an invalid_type issue is given', () => {
expected: 'nan',
received: 'float',
path: ['foo']
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: expected nan, but got float');
});

Expand All @@ -31,7 +31,7 @@ test('returns the formatted issue when an invalid_literal issue is given', () =>
expected: 'foo',
received: 'bar',
path: ['foo']
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: invalid literal: expected "foo", but got string');
});

Expand All @@ -41,7 +41,7 @@ test('returns the formatted issue when an unrecognized_keys issue is given', ()
message: '',
keys: ['bar'],
path: ['foo']
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: unexpected additional property: "bar"');
});

Expand All @@ -53,7 +53,7 @@ test('returns the formatted issue when an too_big issue is given', () => {
inclusive: false,
type: 'string',
maximum: 2
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: string must contain less than 2 characters');
});

Expand All @@ -65,7 +65,7 @@ test('returns the formatted issue when an too_small issue is given', () => {
inclusive: false,
type: 'string',
minimum: 2
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: string must contain more than 2 characters');
});

Expand All @@ -75,7 +75,7 @@ test('returns the formatted issue when an not_multiple_of issue is given', () =>
path: ['foo'],
message: '',
multipleOf: 42
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: number must be multiple of 42');
});

Expand All @@ -86,7 +86,7 @@ test('returns the formatted issue when an invalid_enum_value issue is given', ()
message: '',
options: ['a', 'b'],
received: 1
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: invalid enum value: expected one of "a" or "b", but got number');
});

Expand All @@ -96,7 +96,7 @@ test('returns the formatted issue when an invalid_string issue is given', () =>
path: ['foo'],
message: '',
validation: 'ip'
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: invalid ip');
});

Expand All @@ -106,8 +106,8 @@ test('returns the formatted issue when an invalid_union_discriminator issue is g
path: ['foo'],
message: '',
options: ['a']
});
assert.strictEqual(formattedIssue, 'at foo: invalid discriminator value, expected "a"');
}, { foo: undefined });
assert.strictEqual(formattedIssue, 'at foo: invalid discriminator: expected "a", but got undefined');
});

test('returns the formatted issue when an invalid_union issue is given', () => {
Expand All @@ -116,7 +116,7 @@ test('returns the formatted issue when an invalid_union issue is given', () => {
path: ['foo'],
message: '',
unionErrors: []
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: invalid value doesn’t match expected union');
});

Expand All @@ -126,7 +126,7 @@ test('returns the formatted issue when an invalid_arguments issue is given', ()
path: ['foo'],
message: '',
argumentsError: new ZodError([])
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: invalid function arguments');
});

Expand All @@ -136,7 +136,7 @@ test('returns the formatted issue when an invalid_return_type issue is given', (
path: ['foo'],
message: '',
returnTypeError: new ZodError([])
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: invalid function return type');
});

Expand All @@ -145,7 +145,7 @@ test('returns the formatted issue when an invalid_date issue is given', () => {
code: 'invalid_date',
path: ['foo'],
message: ''
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: invalid date');
});

Expand All @@ -154,7 +154,7 @@ test('returns the formatted issue when a custom issue is given', () => {
code: 'custom',
path: ['foo'],
message: ''
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: invalid input');
});

Expand All @@ -163,7 +163,7 @@ test('returns the formatted issue when an invalid_intersection_types issue is gi
code: 'invalid_intersection_types',
path: ['foo'],
message: ''
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: intersection results could not be merged');
});

Expand All @@ -172,6 +172,6 @@ test('returns the formatted issue when an not_finite issue is given', () => {
code: 'not_finite',
path: ['foo'],
message: ''
});
}, '');
assert.strictEqual(formattedIssue, 'at foo: number must be finite');
});
11 changes: 7 additions & 4 deletions source/zod-error-formatter/format-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { formatTooSmallIssueMessage } from './issue-specific/too-small.js';
import { formatUnrecognizedKeysIssueMessage } from './issue-specific/unrecognized-keys.js';
import { formatPath, isNonEmptyPath } from './path.js';

type FormatterForCode<Code extends ZodIssueCode> = (issue: Extract<ZodIssue, { code: Code; }>) => string;
type FormatterForCode<Code extends ZodIssueCode> = (
issue: Extract<ZodIssue, { code: Code; }>,
input: unknown
) => string;

type FormatterMap = {
readonly [Key in ZodIssueCode]: FormatterForCode<Key>;
Expand Down Expand Up @@ -43,11 +46,11 @@ const issueCodeToFormatterMap: FormatterMap = {
not_finite: formatSimpleMessage('number must be finite')
};

export function formatIssue(issue: ZodIssue): string {
export function formatIssue(issue: ZodIssue, input: unknown): string {
const { path, code } = issue;

const formatter = issueCodeToFormatterMap[code] as (issue: ZodIssue) => string;
const message = formatter(issue);
const formatter = issueCodeToFormatterMap[code] as (issue: ZodIssue, input: unknown) => string;
const message = formatter(issue, input);

if (isNonEmptyPath(path)) {
const formattedPath = formatPath(path);
Expand Down
2 changes: 1 addition & 1 deletion source/zod-error-formatter/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test('formatZodError() takes a zod error and formats all issues', () => {
const result = exampleSchema.safeParse({ foo: 42, bar: '' });

assert.strictEqual(result.success, false);
const formattedError = formatZodError(result.error);
const formattedError = formatZodError(result.error, '');

assert.strictEqual(
formattedError.message,
Expand Down
10 changes: 6 additions & 4 deletions source/zod-error-formatter/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type { TypeOf, ZodError, ZodSchema } from 'zod';
import { formatIssue } from './format-issue.js';
import { createFormattedZodError, type FormattedZodError } from './formatted-error.js';

export function formatZodError(error: ZodError): FormattedZodError {
const formattedIssues = error.issues.map(formatIssue);
export function formatZodError(error: ZodError, input: unknown): FormattedZodError {
const formattedIssues = error.issues.map((issue) => {
return formatIssue(issue, input);
});
return createFormattedZodError(formattedIssues);
}

Expand All @@ -14,7 +16,7 @@ export function parse<Schema extends ZodSchema<unknown>>(schema: Schema, value:
return result.data;
}

throw formatZodError(result.error);
throw formatZodError(result.error, value);
}

type SafeParseSuccessResult<Output> = {
Expand All @@ -39,5 +41,5 @@ export function safeParse<Schema extends ZodSchema<unknown>>(
return { success: true, data: result.data };
}

return { success: false, error: formatZodError(result.error) };
return { success: false, error: formatZodError(result.error, value) };
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ test('formats an invalid union discriminator issue correctly with string options
path: [],
message: '',
options: ['a', 'b', 'c']
});
assert.strictEqual(message, 'invalid discriminator value, expected one of "a", "b" or "c"');
}, '');
assert.strictEqual(message, 'invalid discriminator: expected one of "a", "b" or "c", but got string');
});

test('formats an invalid union discriminator issue correctly with empty options', () => {
Expand All @@ -18,8 +18,8 @@ test('formats an invalid union discriminator issue correctly with empty options'
path: [],
message: '',
options: []
});
assert.strictEqual(message, 'invalid discriminator value, expected unknown');
}, '');
assert.strictEqual(message, 'invalid discriminator: expected unknown, but got string');
});

test('formats an invalid union discriminator issue correctly with one option', () => {
Expand All @@ -28,8 +28,8 @@ test('formats an invalid union discriminator issue correctly with one option', (
path: [],
message: '',
options: ['a']
});
assert.strictEqual(message, 'invalid discriminator value, expected "a"');
}, '');
assert.strictEqual(message, 'invalid discriminator: expected "a", but got string');
});

test('formats an invalid union discriminator issue correctly with boolean option', () => {
Expand All @@ -38,8 +38,8 @@ test('formats an invalid union discriminator issue correctly with boolean option
path: [],
message: '',
options: [false]
});
assert.strictEqual(message, 'invalid discriminator value, expected false');
}, '');
assert.strictEqual(message, 'invalid discriminator: expected false, but got string');
});

test('formats an invalid union discriminator issue correctly with number option', () => {
Expand All @@ -48,8 +48,8 @@ test('formats an invalid union discriminator issue correctly with number option'
path: [],
message: '',
options: [1]
});
assert.strictEqual(message, 'invalid discriminator value, expected 1');
}, '');
assert.strictEqual(message, 'invalid discriminator: expected 1, but got string');
});

test('formats an invalid union discriminator issue correctly with undefined option', () => {
Expand All @@ -58,8 +58,8 @@ test('formats an invalid union discriminator issue correctly with undefined opti
path: [],
message: '',
options: [undefined]
});
assert.strictEqual(message, 'invalid discriminator value, expected undefined');
}, '');
assert.strictEqual(message, 'invalid discriminator: expected undefined, but got string');
});

test('formats an invalid union discriminator issue correctly with symbol without description option', () => {
Expand All @@ -69,8 +69,8 @@ test('formats an invalid union discriminator issue correctly with symbol without
message: '',
// eslint-disable-next-line symbol-description -- no description because we want to actually test this case
options: [Symbol()]
});
assert.strictEqual(message, 'invalid discriminator value, expected Symbol()');
}, '');
assert.strictEqual(message, 'invalid discriminator: expected Symbol(), but got string');
});

test('formats an invalid union discriminator issue correctly with symbol with description option', () => {
Expand All @@ -79,6 +79,26 @@ test('formats an invalid union discriminator issue correctly with symbol with de
path: [],
message: '',
options: [Symbol('foo')]
});
assert.strictEqual(message, 'invalid discriminator value, expected Symbol(foo)');
}, '');
assert.strictEqual(message, 'invalid discriminator: expected Symbol(foo), but got string');
});

test('formats an invalid union discriminator issue correctly printing the received type from nested path', () => {
const message = formatInvalidUnionDiscriminatorIssueMessage({
code: 'invalid_union_discriminator',
path: ['foo', 'bar'],
message: '',
options: ['foo']
}, { foo: { bar: true } });
assert.strictEqual(message, 'invalid discriminator: expected "foo", but got boolean');
});

test('formats an invalid union discriminator issue when the property is missing', () => {
const message = formatInvalidUnionDiscriminatorIssueMessage({
code: 'invalid_union_discriminator',
path: ['foo', 'bar'],
message: '',
options: ['foo']
}, { foo: {} });
assert.strictEqual(message, 'missing property');
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import type { ZodInvalidUnionDiscriminatorIssue } from 'zod';
import { getParsedType, type ZodInvalidUnionDiscriminatorIssue } from 'zod';
import { formatOneOfList } from '../list.js';
import { findValueByPath } from '../path.js';

export function formatInvalidUnionDiscriminatorIssueMessage(issue: ZodInvalidUnionDiscriminatorIssue): string {
const formattedOptions = formatOneOfList(issue.options);
return `invalid discriminator value, expected ${formattedOptions}`;
export function formatInvalidUnionDiscriminatorIssueMessage(
issue: ZodInvalidUnionDiscriminatorIssue,
input: unknown
): string {
const result = findValueByPath(input, issue.path);

if (result.found) {
const formattedOptions = formatOneOfList(issue.options);
const receivedValue = result.value;
const receivedType = getParsedType(receivedValue);
return `invalid discriminator: expected ${formattedOptions}, but got ${receivedType}`;
}

return 'missing property';
}
Loading

0 comments on commit f2e5443

Please sign in to comment.