Skip to content

Fix anyOf/oneOf completion narrowing when a branch is clean-by-omission (#307)#338

Open
bryceschober wants to merge 2 commits into
microsoft:mainfrom
bryceschober:bks-fix-issue-307
Open

Fix anyOf/oneOf completion narrowing when a branch is clean-by-omission (#307)#338
bryceschober wants to merge 2 commits into
microsoft:mainfrom
bryceschober:bks-fix-issue-307

Conversation

@bryceschober

@bryceschober bryceschober commented Jul 10, 2026

Copy link
Copy Markdown

Fixes #307

Summary

IntelliSense value completion for a discriminator property inside anyOf/oneOf over-narrows when one branch has additional required properties that haven't been typed yet — only one valid discriminator value is suggested instead of all of them.

This happens because getMatchingSchemas excludes the AST node currently being typed from validation during completion, so a branch can look "problem-free" purely because there was nothing to check yet, not because it's a genuinely better structural match. testAlternatives' winner-selection then fully discards the sibling branch.

Disclaimer

This may not be the best path to the desired behavior. This path was chosen by CoPilot under direction from a SW engineer unfamiliar with Typescript, this module, and the VS Code architecture.

Fix

  • Added hasExclusion() to ISchemaCollector to detect when we're doing value completion (an exclude node is set) rather than diagnostics.
  • In testAlternatives, when one alternative's "problem-free" status is inconclusive (no problems, but nothing substantive matched either) while the other has real problems, both alternatives' matching schemas are merged instead of letting one fully eliminate the other.
  • Applies to both anyOf and oneOf (including oneOf + an explicit discriminator keyword, common in real-world discriminated-union schemas from tools like Pydantic/OpenAPI generators) — this doesn't affect oneOf's real "exactly one must match" diagnostic, which never runs against an excluded node.
  • Legitimate narrowing based on already-typed sibling data (e.g. the existing 'Complete with oneOf and enums' test) is unaffected.

Tests

Two regression tests added in their own commit (separate from the fix), covering the anyOf and oneOf+discriminator shapes respectively.

Verification

  • Both new tests fail on pre-fix code and pass after the fix.
  • Full suite: npm run compile clean, node --test → 4716 passed / 0 failed / 16 skipped, npm run lint clean.
  • Manually verified in a live VS Code Extension Development Host against a real-world production schema using oneOf + discriminator across dozens of variants.
Process notes for maintainers (optional reading)

The notes below document how this fix was derived, for additional context if useful during review. Not required reading.

Problem (#307)

Value completion for a discriminator-like property inside anyOf (and, as later discovered, oneOf with a discriminator mapping) was over-narrowed when one branch has additional required properties the user hasn't typed yet. Completing a "type" (or similarly-named) discriminator property suggested only one valid value instead of all of them, while the object was still incomplete.

Root cause

getMatchingSchemas excludes the AST node currently being typed from validation during completion, so the node's own incomplete value never counts against it. In testAlternatives (the anyOf/oneOf branch evaluator in jsonParser.ts), this can make a branch look "problem-free" purely because there was nothing to check yet — not because it's a genuinely better structural match. The compare()-based winner-selection logic then fully discarded the sibling branch, even though it's still legitimately reachable.

A separate existing test ("Complete with oneOf and enums") has a similar-looking asymmetric-required-properties shape where full elimination IS correct — there, the elimination is backed by real, already-typed sibling data, not just an artifact of exclusion. The fix has to distinguish these two cases.

Fix

Implemented in src/parser/jsonParser.ts:

  • Added hasExclusion() to ISchemaCollector — true only when the collector was constructed with an exclude node, i.e. during value completion (never during diagnostics/validate()).
  • In testAlternatives, when the collector has an active exclusion and one alternative's problem-free status is "inconclusive" (no problems, but also zero real matched data backing it) while the other alternative has real problems, both alternatives' matching schemas are merged instead of letting one fully discard the other.
  • Initially scoped to anyOf only (!maxOneMatch); generalized to also cover oneOf after discovering that real-world discriminated-union schemas (produced by tools like Pydantic/OpenAPI code generators) commonly use oneOf + an explicit discriminator keyword rather than plain anyOf. The same clean-by-omission bug reproduced identically there. The existing "inconclusive clean match" guard already protects legitimate oneOf narrowing regardless of maxOneMatch, so removing that gate was safe.
  • An earlier, broader attempt lived in jsonCompletion.ts instead (a blanket fallback showing all discriminator values whenever the current value was empty). It fixed IntelliSense incorrectly narrows anyOf branches when one variant requires additional properties #307 but broke the legitimate "Complete with oneOf and enums" narrowing test, so it was reverted in favor of the scoped jsonParser.ts fix above.

Tests

Two regression tests added to src/test/completion.test.ts, split into their own commit (separate from the fix commit):

  1. anyOf with two object branches sharing a discriminator property, one branch requiring an extra property — mirrors the schema shape from the original issue report.
  2. The same shape, but via oneOf + $defs/$ref + an explicit discriminator (propertyName/mapping) keyword — mirrors real-world discriminated-union schemas encountered while validating the fix against a private production codebase.

Both assert that an empty/partial discriminator value offers all valid branch values, and that an exact match still narrows to one (existing behavior preserved).

Verification

  • Confirmed each new test fails on pre-fix code (isolated via git stash) and passes after the fix.
  • Full suite: npm run compile clean; node --test → 4716 passed / 0 failed / 16 skipped (includes the official JSON Schema Test Suite's oneOf/anyOf conformance tests); npm run lint clean.
  • Live-tested in a real VS Code Extension Development Host, linked via npm link to a checkout of microsoft/vscode's json-language-features extension, against a private production schema unrelated to this repo that uses oneOf + discriminator across dozens of variants. Confirmed previously-hidden discriminator values now appear during completion for both a top-level command list and a nested object-type list.

Status

Fix and both regression tests are complete, verified, and pushed as two commits to the contributor's fork branch (tests-only commit, then fix-only commit). PR opened in draft against microsoft/vscode-json-languageservice referencing #307.

Contribution guidelines checklist (from Microsoft/vscode wiki "How to Contribute")

  • CLA: required before merge; handled automatically by a bot on first PR from this account.
  • One PR per issue, issue linked: single PR for IntelliSense incorrectly narrows anyOf branches when one variant requires additional properties #307, body should include Fixes #307.
  • Don't bundle unrelated fixes: the anyOf fix and its oneOf generalization share the same root cause (clean-by-omission during exclusion), so combining them in one PR is appropriate.
  • Small, non-formatting diffs: comments were trimmed to match this repo's actual (terse, non-JSDoc) style rather than the wiki's generic "JSDoc for interfaces" guidance — verified the repo's own public API has no JSDoc on interfaces, so following local convention over the generic wiki guidance was the right call here.
  • Tests included: yes, two regression tests, each in its own commit separate from the implementation.

bryceschober and others added 2 commits July 10, 2026 11:49
anyOf/oneOf branch completion incorrectly narrows discriminator values
when one variant requires additional properties that haven't been
typed yet. These tests reproduce the bug reported in microsoft#307 for both
forms:

- a plain anyOf (matching the shape from the original issue report)
- oneOf with an explicit "discriminator" keyword (the pattern
  real-world discriminated-union schemas, e.g. those emitted by
  Pydantic/OpenAPI generators, commonly use instead of anyOf)

Both assert an empty/partial discriminator value offers all valid
branch values, and an exact match still narrows to one.

This commit intentionally contains only the failing tests, without the
fix, to document the bug in isolation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
getMatchingSchemas excludes the AST node currently being typed from
validation during value completion, so an anyOf/oneOf branch that
requires an additional not-yet-typed property can appear
"problem-free" purely because there was nothing to check yet, not
because it's a genuinely better structural match. testAlternatives'
winner-selection logic then discarded the sibling branch entirely,
causing IntelliSense to only suggest one discriminator value instead
of all valid ones.

Add hasExclusion() to ISchemaCollector so testAlternatives can detect
this exclusion-driven case. When evaluating alternatives during value
completion (collector has an active exclude node) and the "clean"
alternative has zero propertiesValueMatches (nothing about it was
substantively confirmed by real data), merge both alternatives'
matching schemas instead of letting one fully eliminate the other.

This applies equally to anyOf and oneOf: the exclusion-driven
inconclusiveness affects both the same way during completion, and
doesn't affect oneOf's real "exactly one must match" diagnostic, which
never runs against an excluded node. Real-world discriminated-union
schemas (e.g. those emitted by Pydantic/OpenAPI generators) commonly
use oneOf with a discriminator keyword rather than plain anyOf, so both
forms need this fix.

This distinguishes the microsoft#307 scenario (clean only by omission) from
legitimate narrowing based on already-typed sibling data, which
existing tests (e.g. 'Complete with oneOf and enums') continue to
rely on.

Fixes microsoft#307

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@bryceschober

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Dynon Avionics"

@bryceschober

Copy link
Copy Markdown
Author

@aeschli I'm not sure what it would take to get attention for this kind of PR. Can you offer any guidance regarding how I could better help?

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.

IntelliSense incorrectly narrows anyOf branches when one variant requires additional properties

1 participant