Skip to content

fix(schema): avoid JSON.stringify crashes on ArkErrors issues slots#1619

Merged
ssalbdivad merged 9 commits into
arktypeio:mainfrom
gabroberge:fix/arkerrors-tojson-heterogeneous-issues
Jun 30, 2026
Merged

fix(schema): avoid JSON.stringify crashes on ArkErrors issues slots#1619
ssalbdivad merged 9 commits into
arktypeio:mainfrom
gabroberge:fix/arkerrors-tojson-heterogeneous-issues

Conversation

@gabroberge

@gabroberge gabroberge commented May 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Set ArkErrors[Symbol.species] to Array so inherited array methods (map, filter, slice, …) allocate a plain Array, not another ArkErrors. This fixes the case where issues.map(issue => issue.message) could return an ArkErrors whose numeric indices held strings (callback results), which then broke JSON.stringify with TypeError: e.toJSON is not a function inside ArkErrors.prototype.toJSON.
  • Add regression tests: Symbol.species / mapArray, plus a Standard Schema failure path that **JSON.stringify**s a realistic HTTP-style payload ({ issues, messages }).

How the problem shows up

  1. On validation failure, schema["~standard"].validate returns an ArkErrors instance; issues is the same value (an Array subclass).
  2. Without [Symbol.species], issues.map(…) allocates another ArkErrors, filling indices 0..n-1 with callback return values (e.g. strings from issue.message), not ArkError instances.
  3. That value can end up in an HTTP error payload passed to JSON.stringify. ArkErrors defines toJSON, so serialization invokes ArkErrors.prototype.toJSON even when numeric slots are no longer ArkError objects.
  4. ArkErrors#toJSON does this.map(e => e.toJSON()) — strings have no toJSONTypeError: e.toJSON is not a function, often surfacing as a server error while building the response body instead of a stable client error JSON payload.

How this change fixes it

static get [Symbol.species](): ArrayConstructor { return Array } — array methods that create a new array now yield a normal Array, so issues.map(issue => issue.message) returns a plain array of strings. JSON.stringify on issues itself still goes through ArkErrors#toJSON and serializes ArkError instances as before; the failure mode was specifically map (and similar) reusing the subclass constructor for the result.

Test plan

  • Array methods allocate plain Array (Symbol.species), not ArkErrors
  • JSON.stringify round-trip via Standard Schema failure (e.g. HTTP error payload)

…lization

ArkErrors doubles as Standard Schema `issues`; JSON.stringify must not assume
every indexed entry is an ArkError with toJSON (e.g. Nest 12 validation bodies).
Add regression coverage for a plain issue-shaped entry.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix itself is correctly implemented and the branching logic is sound, but the motivating scenario relies on bypassing ArkErrors's ReadonlyArray typing via an as unknown as { push } cast — there is no public path through ArkType that lands a non-ArkError in this array. Before adding a permanent defensive serializer to a hot, public surface, it would help to see a real-world reproduction that produces the TypeError without forcing the cast (e.g. a Standard Schema consumer that legitimately ends up with heterogeneous issues from ArkType output).

TL;DR — Hardens ArkErrors#toJSON so JSON.stringify no longer throws TypeError: e.toJSON is not a function if the array contains entries that don't implement toJSON, and adds a regression test.

Key changes

  • Defensive indexed-issue serializerArkErrors#toJSON now routes each entry through a new private indexedIssueToJson helper that branches on the entry's shape instead of assuming every element is an ArkError.
  • Regression test — pushes a plain { message, path } object into an ArkErrors instance and asserts JSON.parse(JSON.stringify(errors)) round-trips both entries.

Summary | 2 files | 1 commit | base: mainfix/arkerrors-tojson-heterogeneous-issues


Tolerating non-ArkError entries in toJSON

Before: toJSON mapped each entry with e.toJSON(), throwing if any entry lacked the method.
After: Each entry is routed through indexedIssueToJson, which handles undefined / null / non-objects / objects with toJSON / { message, path } shapes / fallback String(issue).

The branch order is sensible and preserves existing ArkError behavior via toJSON.call(issue). The crash described in the PR body, however, requires (errors as unknown as { push }).push(...) — ArkType's own code only inserts via add(error: ArkError), so a real reproduction path from Standard Schema usage would strengthen the case for keeping this defensive code long-term.

ark/schema/shared/errors.ts · ark/schema/__tests__/errors.test.ts

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread ark/schema/__tests__/errors.test.ts
Comment thread ark/schema/__tests__/errors.test.ts Outdated
Comment thread ark/schema/shared/errors.ts Outdated
Comment thread ark/schema/shared/errors.ts Outdated
Comment thread ark/schema/shared/errors.ts Outdated
Document Standard Schema issues contract, assert toJSON returns JsonObject,
only include plain-issue path when it is an array, and rename helper to
indexedIssueToJSON for consistency.
Use a fresh ArkErrors instance, document the contract test intent, and assert
the real ArkError row shape on parsed[0] alongside the foreign issue entry.
Insert leading semicolon before parenthesized expression (Prettier / ASI).
…ect behavior

Introduce tests to verify that Array methods like map, filter, and slice return plain Arrays instead of ArkErrors. This ensures that callbacks returning primitives do not inadvertently create ArkErrors, maintaining the integrity of JSON serialization.
…ation for ArkErrors

Clarify the behavior of inherited array methods and their impact on JSON serialization. Update comments to reflect that `issues` can include plain objects without a `toJSON` method, ensuring compatibility with Standard Schema. Adjust the `indexedIssueToJSON` method to emphasize the handling of mixed issue types during serialization.
@gabroberge gabroberge changed the title fix(schema): avoid JSON.stringify crash on heterogeneous Standard Schema issues fix(schema): avoid JSON.stringify crashes on ArkErrors issues slots May 17, 2026
Refactor the test to validate the serialization of Standard Schema failure issues. Ensure that the test checks for the correct structure of parsed results and that messages are accurately captured, improving coverage for mixed issue types in JSON serialization.
@ssalbdivad

Copy link
Copy Markdown
Member

Great, this was a huge footgun thanks so much for the thoughtful fix ❤️

@ssalbdivad ssalbdivad merged commit 10e4ce3 into arktypeio:main Jun 30, 2026
5 of 6 checks passed
@github-project-automation github-project-automation Bot moved this from To do to Done (merged or closed) in arktypeio Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done (merged or closed)

Development

Successfully merging this pull request may close these issues.

2 participants