fix(zod-v3): rebuild survives a getter-only _def - #637
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
`rebuildWithDef` created the replacement node with
`Object.create(getPrototypeOf(original))` and then assigned
`node._def = {...}`. That assignment is the whole point of the
module: rebuild a node on its OWN prototype so a mismatched
second Zod hoisted into the tree cannot poison the result.
Zod 4.5 moved `_def` from a writable own property to a
getter-only accessor on the prototype. Assigning through an
inherited accessor with no setter throws under the strict mode
every module runs in, so the helper written to survive a
foreign-realm node threw a raw TypeError on exactly that node:
TypeError: Cannot set property _def of #<_>
which has only a getter
Defining an own data property shadows the accessor instead, and
lands on both realms. The flags reproduce what the assignment
produced on Zod v3 (writable, enumerable, configurable), so the
intended v3 path is unchanged.
The existing realm-faithfulness test does cover this, but only
when the hoisted Zod happens to be 4.5 or newer; the lockfile
pins 4.4.3, where it passes vacuously. That is how the
regression got in. The new pin builds the getter-only shape by
hand, so it bites whatever version the tree resolves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnAVwFjpvKQNppAMkSvoiH
Folds the `_def` definition into the `Object.create` that already builds the node, instead of creating the node and then calling `Object.defineProperty` on it. Same semantics, one expression instead of two, and 14 bytes cheaper gzipped, which matters here: dist/index.mjs and dist/zod.mjs sit 40 B under a 53.5 KB cap on main, and the two-statement form landed 3 B over it. main 53460 B 40 B headroom defineProperty 53503 B OVER by 3 this 53489 B 11 B headroom Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnAVwFjpvKQNppAMkSvoiH
ozzyfromspace
force-pushed
the
fix/zod45-getter-only-def
branch
from
September 15, 2026 03:57
6e9521b to
8b6ec0d
Compare
This branch was successfully deployed
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.
Found while triaging Dependabot #630. That PR bumps
apps/bench-arena'szod, which has nothing to do with the v3 adapter, yet it turnedtest/adapters/zod-v3/rebuild-schema.test.tsred:The bump is being closed separately. The TypeError is real and lands here.
What broke
rebuild-schema.tsexists so the adapter never rebuilds a node through an ambient constructor. Its own docblock names the hazard it defends against: "a hoisted Zod v4 beside the Zod v3 a schema was authored with".rebuildWithDefputs the replacement on the original's prototype and patches_def:Zod 4.5 moved
_deffrom a writable own property to a getter-only accessor on the prototype. Measured across the three versions in play:_def_def)Assigning through an inherited setter-less accessor throws under the strict mode every module runs in. So the helper written to survive a foreign-realm node threw a raw
TypeErroron exactly that node, which is a library throw reaching consumer code.The fix
Define the own data property instead of assigning it. That shadows the accessor and lands on both realms. The flags reproduce what the assignment produced on v3 (
writable,enumerable,configurableall true, verified against a real v3 node), so the intended v3 path is byte-for-byte unchanged.Why it got in
The existing realm-faithfulness test does exercise this: it hands
rebuildObjectazod/v4node on purpose. But it only bites when the hoisted Zod is 4.5+, and the lockfile pins rootzodat 4.4.3, where it passes vacuously.^4.3.6floating to 4.5.x is onepnpm updateaway, so the trap is armed.The new pin builds the getter-only shape by hand rather than importing a Zod, so it bites whatever the tree resolves. Verified both ways: with the fix reverted it fails with the same TypeError CI produced; with the fix it passes on the current 4.4.3 lockfile, where the old test cannot.
Gates
pnpm typecheckandpnpm lintclean;test/adapters/zod-v3/23 files / 337 tests green.🤖 Generated with Claude Code
https://claude.ai/code/session_01PnAVwFjpvKQNppAMkSvoiH