Skip to content

fix(schema): don't treat Object.prototype keys as declared for undeclared key checks - #1701

Merged
ssalbdivad merged 2 commits into
arktypeio:mainfrom
thecotne:fix-undeclared-prototype-keys
Oct 20, 2026
Merged

ssalbdivad merged 2 commits into
arktypeio:mainfrom
thecotne:fix-undeclared-prototype-keys

Conversation

@thecotne

Copy link
Copy Markdown
Contributor

Fixes #1699

What

onUndeclaredKey("delete") and onUndeclaredKey("reject") now handle undeclared keys named after Object.prototype members (toString, valueOf, constructor, hasOwnProperty, ...) and an own __proto__ key (as JSON.parse creates) like any other undeclared key. Before, they were kept or accepted.

const T = type({ a: "string" }).onUndeclaredKey("delete")
T({ a: "x", toString: 1 }) // now { a: "x" }, was { a: "x", toString: 1 }

Why

declaresKey and the compiled _compileDeclaresKey check k in propsByKey, and propsByKey was a plain object, so in found inherited members, and "__proto__" in {} is always true.

The fix builds propsByKey as a prototype-free record (Object.create(null)), the same approach as #1586 and #1121, so k in propsByKey stays as it is in both paths. I didn't use Object.hasOwn(propsByKey, k), for two reasons:

  • Object.hasOwn is ES2022 and the repo compiles with lib: ["ES2020"].
  • flatMorph's result[k] = v turns a declared __proto__ key into the record's prototype, so hasOwn would make that key count as undeclared. I tried it locally and the "can be declared" tests below failed.

Writing into a null-prototype object stores __proto__ as an own key, so declared __proto__ keys keep working.

Tests

In ark/type/__tests__/objects/onUndeclaredKey.test.ts, the new tests cover:

  • toString, valueOf, constructor and hasOwnProperty are deleted with "delete" and reported as must be removed with "reject".
  • An own __proto__ key from JSON.parse is handled the same way.
  • Declared toString and declared own __proto__ keys are still kept under both modes, and other extra keys are still rejected.
  • Each case runs with JIT and with jitless: true.

Checklist:

  • Code is up-to-date with the main branch (based on c34bf69)
  • You've successfully run pnpm prChecks locally
  • There are new or updated unit tests validating the change

Not addressed here, possibly a follow-up: required, optional and default presence checks on the data side also use in. So type({ "toString?": "string" })({}) fails with toString must be a string (was an object).

…ared key checks

`declaresKey` and its compiled form check `k in propsByKey`, but
`propsByKey` was a plain object, so inherited names like `toString`,
`constructor` or `hasOwnProperty`, and an own `__proto__` key such as
`JSON.parse` creates, were considered declared. `onUndeclaredKey("delete")`
kept them and `onUndeclaredKey("reject")` accepted them.

Build `propsByKey` as a prototype-free record, so only explicitly declared
keys are found, including a declared `__proto__`.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Prototype-free propsByKey — StructureNode.propsByKey is now built into an Object.create(null) record via reduce instead of flatMorph. Both declaresKey and the compiled _compileDeclaresKey still evaluate k in propsByKey, but the null prototype means inherited Object.prototype members (toString, valueOf, constructor, hasOwnProperty, ...) and an own __proto__ key no longer read as declared, while a declared __proto__ is still stored as an own key.
  • Regression tests — new keys named after Object.prototype members block covers delete and reject for prototype-named keys and for an own __proto__ from JSON.parse, plus declared toString/__proto__, running under both JIT and jitless: true.

I verified the full suite (1795 passing) and confirmed the two new are undeclared cases fail against the unpatched source, so they genuinely pin the bug.

ℹ️ Prototype-named presence checks remain deferred

The data-side presence checks still use in (defaultable at structure.ts:981, and the optional/required handling), so type({ "toString?": "string" })({}) still reports toString as present and fails. This PR correctly scopes to the undeclared-key path and the author already tracks the remainder, so no action is needed here — just noting it so it isn't re-flagged.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Object.fromEntries defines each key as an own property, so a declared
"__proto__" stays a key rather than becoming the prototype, as it
would with flatMorph's assignment. Setting the prototype to null
afterward keeps Object.prototype members like "toString" undeclared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ssalbdivad
ssalbdivad merged commit cd11e1f into arktypeio:main Oct 20, 2026
13 checks passed
@github-project-automation github-project-automation Bot moved this from To do to Done (merged or closed) in arktypeio Oct 20, 2026
@ssalbdivad

Copy link
Copy Markdown
Member

thanks @thecotne, great find! swapped the reduce for Object.fromEntries + setPrototypeOf so it stays a one-liner and declared __proto__ keys still work, then merged

ssalbdivad added a commit that referenced this pull request Oct 20, 2026
Bumps all publishable packages and documents the changes merged since
2.2.8:

- keep tuple elements around defaults in .in and .out (#1696, @ybai08)
- reject NaN in unions discriminated by typeof (#1702, @thecotne)
- don't treat Object.prototype keys as declared (#1701, @thecotne)
- only emit referenced $defs in toJsonSchema (#1689, fixes #1612,
  #1511 and #1546, @giaBaoJS)
- convert unrepresentable types in ~standard.jsonSchema instead of
  throwing (#1694)
- arkregex: allow } to be escaped (#1691, @cpruijsen)
- attest: onlyNamedTestFiles (#1688)

Also moves attest to TypeScript 7.1's stable native API. Starting with
7.1.0-dev.20261009.1, typescript/unstable/{ast,sync,fs} are exported as
typescript/{ast,sync,fs}, so the published 0.57.0 fails on every current
7.1 build with "Package subpath './unstable/ast' is not defined by
exports". The tsconfig paths aliases for the old specifiers kept
resolving them, so typecheck stayed green while attest crashed at
runtime. They are renamed alongside the imports.

The workspace TypeScript moves to 7.1.0-dev.20261020.3, the latest
nightly (no 7.1 beta is published yet).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ssalbdivad added a commit that referenced this pull request Oct 20, 2026
Bumps all publishable packages and documents the changes merged since
2.2.8:

- keep tuple elements around defaults in .in and .out (#1696, @ybai08)
- reject NaN in unions discriminated by typeof (#1702, @thecotne)
- don't treat Object.prototype keys as declared (#1701, @thecotne)
- only emit referenced $defs in toJsonSchema (#1689, fixes #1612,
  #1511 and #1546, @giaBaoJS)
- convert unrepresentable types in ~standard.jsonSchema instead of
  throwing (#1694)
- arkregex: allow } to be escaped (#1691, @cpruijsen)
- attest: onlyNamedTestFiles (#1688)

Also moves attest to TypeScript 7.1's native API paths. Starting with
7.1.0-dev.20261009.1, typescript/unstable/{ast,sync,fs} are exported as
typescript/{ast,sync,fs}, so the published 0.57.0 fails on every current
7.1 build with "Package subpath './unstable/ast' is not defined by
exports". The tsconfig paths aliases for the old specifiers kept
resolving them, so typecheck stayed green while attest crashed at
runtime. They are renamed alongside the imports.

The workspace TypeScript moves to 7.1.0-beta, which exports the same
paths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ssalbdivad added a commit that referenced this pull request Oct 20, 2026
Bumps all publishable packages and documents the changes merged since
2.2.8:

- keep tuple elements around defaults in .in and .out (#1696, @ybai08)
- reject NaN in unions discriminated by typeof (#1702, @thecotne)
- don't treat Object.prototype keys as declared (#1701, @thecotne)
- only emit referenced $defs in toJsonSchema (#1689, fixes #1612,
  #1511 and #1546, @giaBaoJS)
- convert unrepresentable types in ~standard.jsonSchema instead of
  throwing (#1694)
- arkregex: allow } to be escaped (#1691, @cpruijsen)
- attest: onlyNamedTestFiles (#1688)

Also moves attest to TypeScript 7.1's native API paths. Starting with
7.1.0-dev.20261009.1, typescript/unstable/{ast,sync,fs} are exported as
typescript/{ast,sync,fs}, so the published 0.57.0 fails on every current
7.1 build with "Package subpath './unstable/ast' is not defined by
exports". The tsconfig paths aliases for the old specifiers kept
resolving them, so typecheck stayed green while attest crashed at
runtime. They are renamed alongside the imports.

The workspace TypeScript moves to 7.1.0-beta, which exports the same
paths.
ssalbdivad added a commit that referenced this pull request Oct 21, 2026
Ports the tests of cd11e1f from arktype 2.2.9 (#1701, @thecotne).
There, a structure's propsByKey was a plain object, so `toString`,
`constructor` or an own `__proto__` key, as JSON.parse creates, were
found as declared: "+": "delete" kept them and "+": "reject" accepted
them.

This branch already builds propsByKey on a null prototype (02322c9),
so a declared "__proto__" is a key and "toString" isn't, compiled and
jitless alike. The tests come over unchanged and pass; testDifferential
checks the standalone validators of the Types they call.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ssalbdivad added a commit that referenced this pull request Oct 21, 2026
main contributes the 2.2.9 release: version bumps for every publishable
package (arktype 2.2.9, @ark/schema, @ark/util and @ark/fs 0.56.8, attest
0.57.1, arkregex 0.0.14, @ark/json-schema 0.0.13, @ark/fast-check 0.0.20),
attest's move to TypeScript 7.1's native API paths (typescript/{ast,sync,fs}
in place of typescript/unstable/*, with the workspace catalog on
7.1.0-beta), arkdark's regex() highlighting and CI/icon changes, and the
2.2.9 fixes: tuple elements around defaults in .in/.out (#1696), the NaN
check in typeof-discriminated unions (#1702), Object.prototype keys as
undeclared (#1701), only referenced $defs in toJsonSchema (#1689) and
escaped } in arkregex (#1691).

featherduster already had an equivalent of every one of those fixes, each
with main's test or a broader one, so every code conflict keeps
featherduster's side:

- ark/schema constraint.ts, roots/{domain,root,union}.ts,
  structure/{sequence,structure}.ts, shared/toJsonSchema.ts: main patched
  code that featherduster has moved to ark/sets (intersectConstraints,
  discrimination, toJsonSchema) or rewritten. featherduster already narrows
  two constraints with one bound, reduces tuple prefix/optionals around
  defaultables, keeps the NaN check on a typeof case, uses a null-prototype
  propsByKey and resolves only referenced $defs.
- ark/type/__tests__/discrimination.test.ts: main's "number case includes
  NaN" is covered by featherduster's "number case includes NaN and
  Infinity". Its jitless loop is dropped, since testJitless runs the whole
  suite jitless.
- ark/type/__tests__/keywords/number.test.ts: featherduster's messages
  ("must be a number (was NaN)") are kept. A miss of number | null has the
  discriminant code there, so it names only the branch the value reached.
- ark/type/__tests__/match.test.ts: featherduster's caseReference snapshots.
- ark/type/__tests__/toJsonSchema.test.ts: featherduster's referencedDefs
  tests cover main's three useRefs cases (#1546, #1511, #1612).
- CHANGELOGs: featherduster already carried arktype's 2.2.9 and arkregex's
  0.0.14 sections, identical to main's, below its Unreleased sections. In
  attest's, main's 0.57.1 section goes below Unreleased.
- pnpm-lock.yaml: featherduster's, reinstalled against the merged
  catalog, so the only change is typescript 7.1.0-dev.20261006.1 ->
  7.1.0-beta.

The precompiled-module version link moves with the version: runtime.ts
exports precompiledForArktype_2_2_9 in place of precompiledForArktype_2_2_8,
which build.ts's checkVersionExport derives from package.json. The
precompile setup and troubleshooting docs, and llms-full.txt generated
from them, name 2_2_9 and "runs on 2.2.9" in their examples. The docs'
generated util.ts declarations pick up @ark/util 0.56.8.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done (merged or closed)

Development

Successfully merging this pull request may close these issues.

onUndeclaredKey keeps undeclared keys named after Object.prototype members (toString, constructor, __proto__, ...)

2 participants