Skip to content

Commit

Permalink
Improve error message for invalid JSON values (#224)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Mrtenz authored Dec 10, 2024
1 parent 6201c23 commit f3d602a
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 12 deletions.
12 changes: 12 additions & 0 deletions src/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ describe('json', () => {
'Expected a value of type `JSON`, but received: `undefined`',
);
});

it('returns a readable error message for a nested JsonStruct', () => {
const struct = object({
value: JsonStruct,
});

const [error] = validate({ value: undefined }, struct);
assert(error !== undefined);
expect(error.message).toBe(
'At path: value -- Expected a value of type `JSON`, but received: `undefined`',
);
});
});

describe('getSafeJson', () => {
Expand Down
27 changes: 15 additions & 12 deletions src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
union,
unknown,
Struct,
refine,
} from '@metamask/superstruct';
import type {
Context,
Expand Down Expand Up @@ -215,18 +216,20 @@ export const UnsafeJsonStruct: Struct<Json> = define('JSON', (json) =>
* This struct sanitizes the value before validating it, so that it is safe to
* use with untrusted input.
*/
export const JsonStruct = coerce(UnsafeJsonStruct, any(), (value) => {
assertStruct(value, UnsafeJsonStruct);
return JSON.parse(
JSON.stringify(value, (propKey, propValue) => {
// Strip __proto__ and constructor properties to prevent prototype pollution.
if (propKey === '__proto__' || propKey === 'constructor') {
return undefined;
}
return propValue;
}),
);
});
export const JsonStruct = coerce(
UnsafeJsonStruct,
refine(any(), 'JSON', (value) => is(value, UnsafeJsonStruct)),
(value) =>
JSON.parse(
JSON.stringify(value, (propKey, propValue) => {
// Strip __proto__ and constructor properties to prevent prototype pollution.
if (propKey === '__proto__' || propKey === 'constructor') {
return undefined;
}
return propValue;
}),
),
);

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

0 comments on commit f3d602a

Please sign in to comment.