Skip to content

fix(object): retain existing-property attributes in Object.defineProperty - #4697

Merged
proggeramlug merged 1 commit into
mainfrom
fix/defineproperty-retain-attrs
Jun 6, 2026
Merged

proggeramlug merged 1 commit into
mainfrom
fix/defineproperty-retain-attrs

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Object.defineProperty on an existing own property must retain the attribute fields the descriptor omits (per ES OrdinaryDefineOwnProperty / ValidateAndApplyPropertyDescriptor) — they do not reset to the new-property false default. Perry previously defaulted every omitted attribute to false (and writable to has_accessor) on every define call, so converting an enumerable data property to a getter (or back to a value) silently dropped its enumerability / configurability.

This is a general spec-compliance bug, surfaced by zod v4 under perry.compilePackages (discussion #3438).

Root cause

zod's $ZodObject swaps the enumerable shape data property for a self-replacing getter via Object.defineProperty, then relies on { ...def } still spreading shape. Pre-fix, the first defineProperty(def, "shape", { get }) reset enumerable to false, so { ...def } dropped shape, normalizeDef's value.shape was undefined, and the slow-path object parser dereferenced shape[key]._zod on undefined:

TypeError: Cannot read properties of undefined (reading '_zod')

(Node never hits this because it takes the JIT new Function fastpass; Perry correctly throws on new Function, so zod's allowsEval gate is false and it falls to the slow path — which exposed the latent attribute bug.)

Fix

In both the object and closure define paths of js_object_define_property, capture the property's current effective attributes before any mutation (all-true when no descriptor side-table entry exists, matching the JS data-property default) and default each omitted descriptor field to the existing value. Brand-new properties still default to false, preserving prior behavior.

Validation

  • New regression test test-files/test_issue_defineproperty_retain_attrs.ts — byte-matches node --experimental-strip-types. Covers value-only redefine, data→getter→value transitions, configurability retention, the new-property false control, and the exact zod shape-getter→spread pattern.
  • With this fix, real zod v4 (perry.compilePackages: ["zod"]) object / nested-object / array / boolean / optional parsing now works and matches Node — previously every z.object(...).parse(...) crashed.
  • cargo fmt clean.

Scope / follow-ups

This unblocks the core of zod object parsing. Two independent zod runtime bugs remain (separate from this change), to be filed/fixed as follow-ups:

  • z.string().min(2) (checks) → ch._zod.onattach undefined
  • safeParse on invalid input → error is not a function

Refs #3438, #793.

…erty

Per ES OrdinaryDefineOwnProperty / ValidateAndApplyPropertyDescriptor,
redefining an EXISTING own property must retain the attribute fields the
descriptor omits — they do NOT reset to the new-property `false` default.
`js_object_define_property` previously defaulted every omitted attribute to
`false` (writable to `has_accessor`) on every call, so converting an
enumerable data property to a getter (or back to a value) silently dropped
its enumerability and configurability.

This blocked zod v4 under `perry.compilePackages` (discussion #3438): its
`$ZodObject` constructor swaps the enumerable `shape` data property for a
self-replacing getter via `Object.defineProperty`, expecting `{ ...def }`
to still spread `shape`. Pre-fix `shape` became non-enumerable, the spread
dropped it, `normalizeDef`'s `value.shape` was undefined, and object parsing
crashed with "Cannot read properties of undefined (reading '_zod')".

Fix: in both the object and closure define paths, capture the property's
current effective attributes before any mutation (all-true when no descriptor
side-table entry exists, matching the JS data-property default) and default
each omitted descriptor field to the existing value. New properties still
default to `false`, preserving prior behavior.

Adds test-files/test_issue_defineproperty_retain_attrs.ts (byte-matches
`node --experimental-strip-types`).
@proggeramlug
proggeramlug merged commit f1b9c6e into main Jun 6, 2026
13 checks passed
@proggeramlug
proggeramlug deleted the fix/defineproperty-retain-attrs branch June 6, 2026 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant