Skip to content

Commit

Permalink
Merge pull request #2 from enormora/unrec
Browse files Browse the repository at this point in the history
Format unrecognized-keys issues
  • Loading branch information
lo1tuma committed Mar 21, 2024
2 parents 4d901ac + 989585d commit 1122119
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"test": "npm run test:unit:with-coverage && npm run test:integration",
"pretest:unit": "tsc -b source/tsconfig.unit-tests.json",
"test:unit": "mt target/build/source",
"test:unit:with-coverage": "c8 mt target/build/source",
"test:unit:with-coverage": "c8 npm run test:unit",
"pretest:integration": "tsc -b integration-tests/tsconfig.json",
"test:integration": "mt target/build/integration-tests"
},
Expand Down
10 changes: 10 additions & 0 deletions source/zod-error-formatter/format-issue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ test('returns the formatted issue when an invalid_literal issue is given', () =>
});
assert.strictEqual(formattedIssue, 'at foo: invalid literal: expected "foo", but got string');
});

test('returns the formatted issue when an unrecognized_keys issue is given', () => {
const formattedIssue = formatIssue({
code: 'unrecognized_keys',
message: '',
keys: ['bar'],
path: ['foo']
});
assert.strictEqual(formattedIssue, 'at foo: unexpected additional property: "bar"');
});
4 changes: 3 additions & 1 deletion source/zod-error-formatter/format-issue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ZodIssue, ZodIssueCode } from 'zod';
import { formatInvalidLiteralIssueMessage } from './issue-specific/invalid-literal.js';
import { formatInvalidTypeIssueMessage } from './issue-specific/invalid-type.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;
Expand All @@ -11,7 +12,8 @@ type FormatterMap = {

const issueCodeToFormatterMap: FormatterMap = {
invalid_type: formatInvalidTypeIssueMessage,
invalid_literal: formatInvalidLiteralIssueMessage
invalid_literal: formatInvalidLiteralIssueMessage,
unrecognized_keys: formatUnrecognizedKeysIssueMessage
};

export function formatIssue(issue: ZodIssue): string {
Expand Down
12 changes: 6 additions & 6 deletions source/zod-error-formatter/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ test('formatZodError() takes a zod error and formats all issues', () => {
formattedError.message,
stripIndents`Validation failed with 2 issues:
- at foo: expected string, but got number
- Unrecognized key(s) in object: 'bar'`
- unexpected additional property: "bar"`
);
assert.deepStrictEqual(formattedError.issues, [
'at foo: expected string, but got number',
"Unrecognized key(s) in object: 'bar'"
'unexpected additional property: "bar"'
]);
});

Expand All @@ -34,11 +34,11 @@ test('parse() parses a given value with a given schema and throws the formatted
(error as Error).message,
stripIndents`Validation failed with 2 issues:
- at foo: expected string, but got number
- Unrecognized key(s) in object: 'bar'`
- unexpected additional property: "bar"`
);
assert.deepStrictEqual((error as FormattedZodError).issues, [
'at foo: expected string, but got number',
"Unrecognized key(s) in object: 'bar'"
'unexpected additional property: "bar"'
]);
}
});
Expand All @@ -56,11 +56,11 @@ test('safeParse() parses a given value with a given schema and returns a failure
result.error.message,
stripIndents`Validation failed with 2 issues:
- at foo: expected string, but got number
- Unrecognized key(s) in object: 'bar'`
- unexpected additional property: "bar"`
);
assert.deepStrictEqual(result.error.issues, [
'at foo: expected string, but got number',
"Unrecognized key(s) in object: 'bar'"
'unexpected additional property: "bar"'
]);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { test } from '@sondr3/minitest';
import assert from 'node:assert';
import { formatUnrecognizedKeysIssueMessage } from './unrecognized-keys.js';

test('formats a message which says the property is unknown when the given keys is an empty array', () => {
const message = formatUnrecognizedKeysIssueMessage({
code: 'unrecognized_keys',
path: [],
message: '',
keys: []
});
assert.strictEqual(message, 'unexpected additional property: unknown');
});

test('properly escapes the keys so it doesn’t get confused with unknown when the key is unknown', () => {
const message = formatUnrecognizedKeysIssueMessage({
code: 'unrecognized_keys',
path: [],
message: '',
keys: ['unknown']
});
assert.strictEqual(message, 'unexpected additional property: "unknown"');
});

test('properly escapes the keys in case they contain special characters', () => {
const message = formatUnrecognizedKeysIssueMessage({
code: 'unrecognized_keys',
path: [],
message: '',
keys: ['foo"bar']
});
assert.strictEqual(message, 'unexpected additional property: "foo\\"bar"');
});

test('correctly formats two properties using "and" as a separator', () => {
const message = formatUnrecognizedKeysIssueMessage({
code: 'unrecognized_keys',
path: [],
message: '',
keys: ['foo', 'bar']
});
assert.strictEqual(message, 'unexpected additional properties: "foo" and "bar"');
});

test('correctly formats three properties using comma as separator and "and" for the last item', () => {
const message = formatUnrecognizedKeysIssueMessage({
code: 'unrecognized_keys',
path: [],
message: '',
keys: ['foo', 'bar', 'baz']
});
assert.strictEqual(message, 'unexpected additional properties: "foo", "bar" and "baz"');
});
32 changes: 32 additions & 0 deletions source/zod-error-formatter/issue-specific/unrecognized-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ZodUnrecognizedKeysIssue } from 'zod';
import { isNonEmptyArray, type NonEmptyArray } from '../non-empty-array.js';

function joinList(values: NonEmptyArray<string>, separator: string, lastItemSeparator: string): string {
const initialList = Array.from(values);
const lastItem = initialList.pop() as string;

if (initialList.length === 0) {
return lastItem;
}

const joinedInitialList = initialList.join(separator);
return `${joinedInitialList}${lastItemSeparator}${lastItem}`;
}

function stringify(value: unknown): string {
return JSON.stringify(value);
}

function formatList(values: readonly string[]): string {
const escapedValues = values.map(stringify);
if (!isNonEmptyArray(escapedValues)) {
return 'unknown';
}
return joinList(escapedValues, ', ', ' and ');
}

export function formatUnrecognizedKeysIssueMessage(issue: ZodUnrecognizedKeysIssue): string {
const formattedProperties = formatList(issue.keys);
const label = issue.keys.length > 1 ? 'properties' : 'property';
return `unexpected additional ${label}: ${formattedProperties}`;
}
5 changes: 5 additions & 0 deletions source/zod-error-formatter/non-empty-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type NonEmptyArray<Item> = readonly [Item, ...(readonly Item[])];

export function isNonEmptyArray<Item>(list: readonly Item[]): list is NonEmptyArray<Item> {
return list.length > 0;
}
8 changes: 4 additions & 4 deletions source/zod-error-formatter/path.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { isNonEmptyArray, type NonEmptyArray } from './non-empty-array.js';

type PathItem = number | string;

type Path = readonly [PathItem, ...readonly PathItem[]];
type Path = NonEmptyArray<PathItem>;

export function isNonEmptyPath(path: readonly PathItem[]): path is Path {
return path.length > 0;
}
export const isNonEmptyPath = isNonEmptyArray<PathItem>;

export function formatPath(path: Path): string {
return path.reduce<string>((currentFormattedPath, item, index) => {
Expand Down

0 comments on commit 1122119

Please sign in to comment.