Skip to content

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

Open
gabroberge wants to merge 6 commits into
arktypeio:mainfrom
gabroberge:fix/arkerrors-tojson-heterogeneous-issues
Open

fix(schema): avoid JSON.stringify crashes on ArkErrors issues slots#1619
gabroberge wants to merge 6 commits into
arktypeio:mainfrom
gabroberge:fix/arkerrors-tojson-heterogeneous-issues

Conversation

@gabroberge
Copy link
Copy Markdown

@gabroberge gabroberge commented May 17, 2026

Summary

  • Set ArkErrors[Symbol.species] to Array so Array.prototype.map / filter / slice / … allocate a plain Array, not another ArkErrors. This fixes the case where issues.map(issue => issue.message) (or similar) could produce an ArkErrors whose numeric indices held strings (callback results), which then reached JSON.stringify and triggered TypeError: e.toJSON is not a function inside ArkErrors.prototype.toJSON.
  • Harden ArkErrors#toJSON: each indexed entry is serialized via indexedIssueToJSON, which tolerates primitives and plain { message, path? } shapes (defensive complement at the JSON boundary).
  • Add regression tests: Symbol.species / mapArray, plus round-trip JSON.stringify coverage including a simulated non-ArkError slot for the defensive path.

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(…) can allocate 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 inside an HTTP error payload that is later passed to JSON.stringify. ArkErrors defines toJSON, so serialization invokes ArkErrors.prototype.toJSON even when numeric slots are no longer ArkError objects.
  4. A naive this.map(e => e.toJSON()) assumes every e is an ArkErrorTypeError: 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

  1. 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 (or similar), not a new ArkErrors with primitives at numeric indices.
  2. ArkErrors#toJSON maps each slot with indexedIssueToJSON:
    • undefined / null{ message: "undefined" | "null" }
    • non-objects{ message: String(issue) }
    • objects with callable toJSONtoJSON.call(issue) as JsonObject
    • objects with string message: { message }, or { message, path } only when path is undefined or an array (typed as Json)
    • otherwise → { message: String(issue) }
  3. Together, the map → species** path no longer crashes during **JSON.stringify**, and **toJSON** stays a small safety net for heterogeneous **issues` at the HTTP/JSON edge.

Note on the defensive toJSON path

The indexedIssueToJSON helper is intentionally broader than strict ArkError-only output: it keeps JSON.stringify from throwing on primitives or plain issue-shaped objects at an index. [Symbol.species] addresses the main failure mode (array methods materializing another ArkErrors full of strings). In principle, maintainers could drop the defensive branch later and revert to e.toJSON()-only serialization if the project prefers a stricter surface and accepts that only “well-formed” ArkErrors contents are supported at that boundary—at the cost of less resilience to odd issues shapes from outside callers.

…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.
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To do

Development

Successfully merging this pull request may close these issues.

1 participant