Repository navigation
fix(schema): don't treat Object.prototype keys as declared for undeclared key checks - #1701
Conversation
…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__`.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Prototype-free
propsByKey—StructureNode.propsByKeyis now built into anObject.create(null)record viareduceinstead offlatMorph. BothdeclaresKeyand the compiled_compileDeclaresKeystill evaluatek in propsByKey, but the null prototype means inheritedObject.prototypemembers (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 membersblock coversdeleteandrejectfor prototype-named keys and for an own__proto__fromJSON.parse, plus declaredtoString/__proto__, running under both JIT andjitless: 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.
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>
|
thanks @thecotne, great find! swapped the reduce for |
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>
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>
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.
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>
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>

Fixes #1699
What
onUndeclaredKey("delete")andonUndeclaredKey("reject")now handle undeclared keys named afterObject.prototypemembers (toString,valueOf,constructor,hasOwnProperty, ...) and an own__proto__key (asJSON.parsecreates) like any other undeclared key. Before, they were kept or accepted.Why
declaresKeyand the compiled_compileDeclaresKeycheckk in propsByKey, andpropsByKeywas a plain object, soinfound inherited members, and"__proto__" in {}is alwaystrue.The fix builds
propsByKeyas a prototype-free record (Object.create(null)), the same approach as #1586 and #1121, sok in propsByKeystays as it is in both paths. I didn't useObject.hasOwn(propsByKey, k), for two reasons:Object.hasOwnis ES2022 and the repo compiles withlib: ["ES2020"].flatMorph'sresult[k] = vturns a declared__proto__key into the record's prototype, sohasOwnwould 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,constructorandhasOwnPropertyare deleted with"delete"and reported asmust be removedwith"reject".__proto__key fromJSON.parseis handled the same way.toStringand declared own__proto__keys are still kept under both modes, and other extra keys are still rejected.jitless: true.Checklist:
mainbranch (based on c34bf69)pnpm prCheckslocallyNot addressed here, possibly a follow-up: required, optional and default presence checks on the data side also use
in. Sotype({ "toString?": "string" })({})fails withtoString must be a string (was an object).