fix(stdlib): keep recursive zod schemas identity-stable - #25364
Open
spalladino wants to merge 2 commits into
Open
fix(stdlib): keep recursive zod schemas identity-stable#25364spalladino wants to merge 2 commits into
spalladino wants to merge 2 commits into
Conversation
`NestedProcessReturnValues.schema` and `PrivateCallExecutionResult.schema` refer to themselves through `z.lazy`, and both getters built a brand new schema on every access. zod ties a recursive schema together by object identity, so from 4.5.0 onwards a lazy target that hands back a fresh schema recurses until the stack overflows — at any nesting depth, including none. The published packages depend on `zod: ^4`, so an npm install picks up 4.5.x and every PXE call that parses a public simulation output or a private execution result dies with `RangeError: Maximum call stack size exceeded`. The monorepo's yarn.lock pins 4.4.3, which is why nothing in yarn-project CI sees it. Both schemas are now built once on first use and reused.
Collaborator
Flakey Tests🤖 says: This CI run detected 1 tests that failed, but were tolerated due to a .test_patterns.yml entry. |
Every workspace declared `"zod": "^4"`, so the published packages accepted any 4.x. zod 4.5.0 broke recursive schemas built with `z.lazy`, and because the monorepo's yarn.lock pinned 4.4.3, nothing in CI saw it — only the release tests and real users, who resolve the range fresh at install time. Pinning to `~4.4.3` makes the range say what the lockfile already enforced, so a zod minor release can no longer change behaviour for consumers without a deliberate bump here. The lockfile resolution is unchanged.
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.
The symptom
Any wallet/PXE talking to a node with npm-installed Aztec packages dies as soon as it simulates a transaction:
It shows up in the
aztec-uprelease tests (bridge_and_claim) and for anyone installing the publishedpackages. It does not show up anywhere in yarn-project CI.
What is going on
Two schemas describe recursive data and refer to themselves through
z.lazy:NestedProcessReturnValues— return values of a public call treePrivateCallExecutionResult— a private call and its nested callsBoth were written as
static get schema()that builds a brand new schema object on every access:zod ties a recursive schema together by object identity: the lazy target is expected to be the same
schema instance each time it is dereferenced. Up to zod 4.4.x this only cost extra allocations. From
4.5.0 onwards the schema graph is walked eagerly, so a lazy target that hands back a fresh instance
every time never closes the loop and recurses until the stack overflows — at any nesting depth, including
none at all (
{ nested: [] }is enough).Why it is invisible in CI: the monorepo's
yarn.lockpins zod 4.4.3, but every package declares"zod": "^4". The release tests and real users npm-install the published packages, so they resolvewhatever is latest — zod 4.5.0 was published on 2026-08-28, and CI started failing on the first
aztec-uprun after that.When was this introduced
The pattern is old, the breakage is new — and it is not the zod 4 migration.
NestedProcessReturnValues.schemawritten as a rebuilt-per-access recursive getterPrivateCallExecutionResult.schemawritten the same wayaztec-upCI run after thatVerified directly by parsing a rebuilt-per-access recursive schema against each release: zod 3.25.76,
4.0.0 and 4.4.3 all accept it; 4.5.0, 4.5.2 and 4.5.4 all overflow the stack. So the
schemas were latently wrong for ~21 months, survived the zod 4 migration untouched, and only started
failing when 4.5.0 began walking the schema graph eagerly. Nothing in this repo changed — the dependency
did.
The fix
Build each schema once on first use and hand out that same instance:
Construction stays lazy (no module-eval-time schema building, so no new circular-import hazards), and the
recursion now closes on a stable object.
The other
z.lazyuses in the tree (AbiValueSchema,AbiTypeSchema,AbiDecodedSchema,callTraceSchema) already point at a single module-level schema, and were verified to parse fine underzod 4.5.x.
Pinning zod to a minor
Fixing the schemas removes today's breakage; the range is what let a dependency change behaviour for users
without anyone here choosing it. All 17 workspaces declared
"zod": "^4", so the published packages acceptany 4.x while the monorepo's lockfile quietly held 4.4.3 — CI and consumers were never running the same
zod, which is precisely why this reached users without a single failing test in yarn-project.
The range is now
~4.4.3everywhere: patch releases still flow, a minor bump has to be deliberate. Thelockfile resolution is unchanged (4.4.3), so the only lockfile diff is the descriptor rename — no
transitive churn.
Verification
With zod 4.5.2 swapped into
node_moduleslocally:RangeError: Maximum call stack size exceeded— the exact CI error;Reproduced independently against zod 4.5.0, 4.5.2 and 4.5.4 (all fail on a rebuilt-per-access lazy target,
all pass on a stable one); zod 4.4.3 tolerates both.
Each schema also gets a regression test asserting it hands out the same instance on every access, which is
the invariant that keeps the recursion closed.
Note
Split out of #25357, whose CI surfaced this. Unrelated to that PR's changes — the failure reproduces on the
base branch with zod 4.5.x installed.