Skip to content

Commit f3d602a

Browse files
authored
Improve error message for invalid JSON values (#224)
Currently we use `assertStruct` in the `JsonStruct`, to ensure that the value is JSON-serialisable before coercing it. `assertStruct` returns a generic `AssertionError`, and Superstruct doesn't have any information about where the error was thrown (such as the path). Given the following struct for example: ```ts const ExampleStruct = object({ value: JsonStruct, }); ``` An invalid `value` would result in an `AssertionError` with the following message: > Assertion failed: Expected a value of type `JSON`, but received: `undefined`. After this change, a `StructError` is thrown instead, with the following message: > At path: value -- Expected a value of type `JSON`, but received: `undefined`. This makes it more clear that the error happens at `value`, and it also makes more sense to throw a `StructError` in this case.
1 parent 6201c23 commit f3d602a

File tree

2 files changed

+27
-12
lines changed

2 files changed

+27
-12
lines changed

src/json.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,18 @@ describe('json', () => {
239239
'Expected a value of type `JSON`, but received: `undefined`',
240240
);
241241
});
242+
243+
it('returns a readable error message for a nested JsonStruct', () => {
244+
const struct = object({
245+
value: JsonStruct,
246+
});
247+
248+
const [error] = validate({ value: undefined }, struct);
249+
assert(error !== undefined);
250+
expect(error.message).toBe(
251+
'At path: value -- Expected a value of type `JSON`, but received: `undefined`',
252+
);
253+
});
242254
});
243255

244256
describe('getSafeJson', () => {

src/json.ts

+15-12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
union,
1717
unknown,
1818
Struct,
19+
refine,
1920
} from '@metamask/superstruct';
2021
import type {
2122
Context,
@@ -215,18 +216,20 @@ export const UnsafeJsonStruct: Struct<Json> = define('JSON', (json) =>
215216
* This struct sanitizes the value before validating it, so that it is safe to
216217
* use with untrusted input.
217218
*/
218-
export const JsonStruct = coerce(UnsafeJsonStruct, any(), (value) => {
219-
assertStruct(value, UnsafeJsonStruct);
220-
return JSON.parse(
221-
JSON.stringify(value, (propKey, propValue) => {
222-
// Strip __proto__ and constructor properties to prevent prototype pollution.
223-
if (propKey === '__proto__' || propKey === 'constructor') {
224-
return undefined;
225-
}
226-
return propValue;
227-
}),
228-
);
229-
});
219+
export const JsonStruct = coerce(
220+
UnsafeJsonStruct,
221+
refine(any(), 'JSON', (value) => is(value, UnsafeJsonStruct)),
222+
(value) =>
223+
JSON.parse(
224+
JSON.stringify(value, (propKey, propValue) => {
225+
// Strip __proto__ and constructor properties to prevent prototype pollution.
226+
if (propKey === '__proto__' || propKey === 'constructor') {
227+
return undefined;
228+
}
229+
return propValue;
230+
}),
231+
),
232+
);
230233

231234
/**
232235
* Check if the given value is a valid {@link Json} value, i.e., a value that is

0 commit comments

Comments
 (0)