refactor: migrate Serializable type to Zod schema and fix circular reference handling#12865
Merged
kangfenmao merged 5 commits intomainfrom Feb 12, 2026
Merged
refactor: migrate Serializable type to Zod schema and fix circular reference handling#12865kangfenmao merged 5 commits intomainfrom
kangfenmao merged 5 commits intomainfrom
Conversation
- Replace manual type definition with Zod schema using z.lazy() - Add SerializableSchema export for runtime validation - Add assertSerializable() and tryParseSerializable() helpers - Fix error.ts to use safeSerialize() for response field - Remove FIXME comment about TS2589 recursion Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <[email protected]> Co-Authored-By: Happy <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Circular references cause JSON.stringify to throw, so returning true was misleading. Now correctly returns false. Co-Authored-By: Claude Opus 4.6 <[email protected]>
DeJeune
approved these changes
Feb 10, 2026
kangfenmao
approved these changes
Feb 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Before this PR:
Serializabletype andisSerializablefunction were maintained separatelySerializableValueusedanyworkaround (with FIXME comment about TS2589)error.tsdirectly assignederror.responsewithout serialization, causing type errors withDateobjectsisSerializableincorrectly returnedtruefor circular references, despiteJSON.stringifythrowing on themAfter this PR:
isSerializabletype guard viaz.custom(), ensuring consistent validation behaviorSerializableSchemaexport for use by other modulesassertSerializable,tryParseSerializable) to keep exports minimalerror.tsto properly serializeresponsefield usingsafeSerialize()isSerializableto correctly reject circular referencesisSerializablereturn type tovalue is Serializablefor better type narrowingisSerializableandSerializableSchemaFixes #
Why we need it and why it was done in this way
The following tradeoffs were made:
z.custom(isSerializable)instead of building a recursive Zod schema withz.lazy(), to guarantee the Zod schema and the hand-written type guard share identical validation logic (e.g. circular reference handling, prototype chain checks, built-in object rejection)isSerializable()implementation as the single source of truth for runtime checksThe following alternatives were considered:
z.lazy()+z.union()+z.record(), but this had subtle behavioral differences fromisSerializable()(e.g.z.record()does not check prototype chains or rejectDate/class instances)z.inferto derive type from schema, butz.lazy()requires explicit type annotation to avoidanyinferencez.json()— this was evaluated as a potential replacement for the entire hand-writtenisSerializable+z.custom()approach.z.json()validates the same recursive union (string | number | boolean | null | JsonValue[] | Record<string, JsonValue>), which is structurally identical to ourSerializabletype. However, it was not adopted for the following reasons:z.json()is built onz.lazy(), which recurses without cycle detection. Passing a circular reference causes a stack overflow instead of returning a validation failure. OurisSerializable()uses aseenSet to detect cycles and safely returnsfalse. This is critical becauseutils/serialize.tscallsisSerializable()on arbitrary values of unknown provenance.typeofcomparisons and a single recursive walk.z.json()usesz.union([...])which must attempt each branch in order, adding overhead for deep/large objects.isSerializable()explicitly rejectsDate,RegExp,Map,Set,Error,File, andBlobwith clearinstanceofchecks and a prototype chain guard, making the rejection reasons self-documenting.z.json()rejects them implicitly (they don't match any union branch), which is correct but less obvious to readers.Breaking changes
None. All existing exports remain unchanged:
Serializabletype (same signature)isSerializablefunction (same behavior for all valid inputs; circular references now correctly returnfalseinstead oftrue— this is a bug fix, as circular references are not JSON-serializable andtryLenientSerializealready expects them to be rejected)Special notes for your reviewer
error.tschange (line 196) fixes a type error that surfaced after makingSerializablestricter —error.responseisLanguageModelResponseMetadatawhich containsDateobjects, so it needssafeSerialize()instead of direct assignment.SerializableSchemausesz.custom()to delegate all validation toisSerializable, so the two are guaranteed to be consistent.isSerializablepreviously returnedtruefor circular references to "avoid infinite recursion", but this was incorrect — circular references causeJSON.stringifyto throw, so they are by definition not serializable. ThetryLenientSerializedesign also assumes circular references are not serializable.Checklist
Release note
🤖 Generated with Claude Code
Co-Authored-By: Claude [email protected]