fix(schema): avoid JSON.stringify crashes on ArkErrors issues slots#1619
Conversation
…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.
There was a problem hiding this comment.
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 serializer —
ArkErrors#toJSONnow routes each entry through a new privateindexedIssueToJsonhelper that branches on the entry's shape instead of assuming every element is anArkError. - Regression test — pushes a plain
{ message, path }object into anArkErrorsinstance and assertsJSON.parse(JSON.stringify(errors))round-trips both entries.
Summary | 2 files | 1 commit | base: main ← fix/arkerrors-tojson-heterogeneous-issues
Tolerating non-ArkError entries in toJSON
Before:
toJSONmapped each entry withe.toJSON(), throwing if any entry lacked the method.
After: Each entry is routed throughindexedIssueToJson, which handlesundefined/null/ non-objects / objects withtoJSON/{ message, path }shapes / fallbackString(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
Claude Opus | 𝕏
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.
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.
|
Great, this was a huge footgun thanks so much for the thoughtful fix ❤️ |

Summary
ArkErrors[Symbol.species]toArrayso inherited array methods (map,filter,slice, …) allocate a plainArray, not anotherArkErrors. This fixes the case whereissues.map(issue => issue.message)could return anArkErrorswhose numeric indices held strings (callback results), which then brokeJSON.stringifywithTypeError: e.toJSON is not a functioninsideArkErrors.prototype.toJSON.Symbol.species/map→Array, plus a Standard Schema failure path that **JSON.stringify**s a realistic HTTP-style payload ({ issues, messages }).How the problem shows up
schema["~standard"].validatereturns anArkErrorsinstance;issuesis the same value (anArraysubclass).[Symbol.species],issues.map(…)allocates anotherArkErrors, filling indices0..n-1with callback return values (e.g. strings fromissue.message), notArkErrorinstances.JSON.stringify.ArkErrorsdefinestoJSON, so serialization invokesArkErrors.prototype.toJSONeven when numeric slots are no longerArkErrorobjects.ArkErrors#toJSONdoesthis.map(e => e.toJSON())— strings have notoJSON→TypeError: 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 normalArray, soissues.map(issue => issue.message)returns a plain array of strings.JSON.stringifyonissuesitself still goes throughArkErrors#toJSONand serializesArkErrorinstances as before; the failure mode was specificallymap(and similar) reusing the subclass constructor for the result.Test plan
Array methods allocate plain Array (Symbol.species), not ArkErrorsJSON.stringify round-trip via Standard Schema failure (e.g. HTTP error payload)