Reject null in vObjAny() - #331571
Conversation
TypeofValidator compares `typeof content` against the expected type name.
`typeof null === 'object'`, so vObjAny() — built as
`new TypeofValidator('object')` — accepted null and returned it as a
successful validation:
vObjAny().validate(null) -> { content: null, error: undefined }
Its declared type is ValidatorBase<object>, and TypeScript's `object`
excludes null, so callers trusting the result get a null the types say
cannot be there. Its own getJSONSchema() reports `{ type: 'object' }`,
which in JSON Schema also excludes null.
ObjValidator, in the same file, already guards this:
if (typeof content !== 'object' || content === null) { ... }
and the one in-tree caller re-checks after a successful validate:
const r = vObjAny().validate(input);
if (r.error || r.content === null || Array.isArray(r.content)) {
return undefined;
}
Reject null for the 'object' case, and report it as `null` rather than
`object` so the message is accurate for every typeof validator —
`vString().validate(null)` previously said "Expected string, but got
object".
Adds validation.test.ts; the module had no tests.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates the validation utilities to treat null as a distinct type in typeof-based validators (especially for 'object'), and adds regression tests to ensure null is rejected with a clear error message.
Changes:
- Adjusted
TypeofValidator.validateto rejectnullwhen validating'object'and to reportnullin error messages instead of'object'. - Added unit tests covering
vObjAny()null rejection andvString()/vNumber()null error messaging.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/vs/base/test/common/validation.test.ts | Adds tests validating null handling and error messages for object and primitive validators. |
| src/vs/base/common/validation.ts | Fixes typeof-based validation to reject null for 'object' and improves error messaging to print null. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // `typeof null === 'object'`, so a plain typeof check lets null through vObjAny(), | ||
| // whose declared type and `{ type: 'object' }` schema both exclude it. ObjValidator | ||
| // rejects null the same way. |
There was a problem hiding this comment.
Done in c4c8255.
The comment now states the typeof null === 'object' pitfall itself rather than naming vObjAny()/ObjValidator, and actualType is hoisted.
Hoisting it also let the second condition go: 'null' is never a TypeOfMap key, so actualType !== this.type rejects null on its own. Same behaviour for every type and identical messages -- just one comparison instead of two.
| // whose declared type and `{ type: 'object' }` schema both exclude it. ObjValidator | ||
| // rejects null the same way. | ||
| if (typeof content !== this.type || (this.type === 'object' && content === null)) { | ||
| return { content: undefined, error: { message: `Expected ${this.type}, but got ${content === null ? 'null' : typeof content}` } }; |
There was a problem hiding this comment.
Done in c4c8255.
The comment now states the typeof null === 'object' pitfall itself rather than naming vObjAny()/ObjValidator, and actualType is hoisted.
Hoisting it also let the second condition go: 'null' is never a TypeOfMap key, so actualType !== this.type rejects null on its own. Same behaviour for every type and identical messages -- just one comparison instead of two.
|
@microsoft-github-policy-service agree |
The note described vObjAny()/ObjValidator from inside the generic TypeofValidator; state the typeof null pitfall itself instead. Hoisting `actualType` also removes the need for the second condition: 'null' never equals a TypeOfMap key, so comparing it to this.type rejects null on its own. Same behaviour, same messages, one comparison.
Problem
TypeofValidatorcomparestypeof contentagainst the expected type name:typeof null === 'object', andvObjAny()isnew TypeofValidator('object'). So null passes validation:Two things that makes wrong:
ValidatorBase<object>, and TypeScript'sobjectexcludesnull. A caller trusting the result gets anullthe types say cannot be there.getJSONSchema()returns{ type: 'object' }, and in JSON Schemaobjectexcludes null too (nullis a separate type). The validator contradicts the schema it advertises.ObjValidator, in the same file, already guards exactly this:And the one in-tree caller works around it, re-checking after a successful validation —
src/vs/platform/agentHost/node/claude/claudeSubagentResolver.ts:That
r.content === nullbranch only exists becausevObjAny()lets null through.Change
Reject null for the
'object'case, and report it asnullrather thanobjectso the message is accurate for every typeof validator.vString().validate(null)previously said "Expected string, but got object".Before / after, running the real module:
vObjAny().validate(null)OK content=nullExpected object, but got nullvObjAny().validate({a:1})OKOK(unchanged)vObjAny().validate([1,2])OKOK(unchanged)vObjAny().validate('str')Expected object, but got stringvString().validate(null)Expected string, but got objectExpected string, but got nullArrays still pass, as before — only null changes.
I left the
r.content === nullcheck inclaudeSubagentResolver.tsalone. It is now redundant but still correct, and it guards untrusted SDK JSON; removing it felt like the wrong thing to bundle into this fix.Tests
Added
src/vs/base/test/common/validation.test.ts— the module had no tests. It covers null rejection, that objects and arrays still pass, non-object rejection, and the correctednullmessage on the other typeof validators.Verification
I could not run the repo's test harness — I worked from a sparse checkout (
src/vs/base,src/vs/platform,src/vs/editor/common) because a full clone kept timing out here, so there is nonode_modulesto build against. What I did run:validation.tsmodule directly to produce the before/after table above, with the fix stashed and unstashed.tsc --strict --noEmitonvalidation.ts: no errors.tscreports forvalidation.test.tsare allCannot find name 'suite'/'test'andCannot find module 'assert'. I checked this is environmental: running the identical command against the existingsrc/vs/base/test/common/numbers.test.tsproduces 9 errors of the same class.So the test file has not been executed by the real runner. Worth a CI run before merging, and I am happy to adjust if it disagrees with local conventions.
Note
TypeOfMapalso has anull: nullentry, but no validator is ever constructed with it, and it could not work as written for the sametypeofreason. Left alone as out of scope — flagging it in case it is meant to be used.