Skip to content

fix(compilers/openapi): keep unions/allOf beside a $ref - #461

Open
fuad-daoud wants to merge 2 commits into
mainfrom
fix/union-beside-ref-kept
Open

fix(compilers/openapi): keep unions/allOf beside a $ref#461
fuad-daoud wants to merge 2 commits into
mainfrom
fix/union-beside-ref-kept

Conversation

@fuad-daoud

Copy link
Copy Markdown
Collaborator

Summary

A schema that writes oneOf/anyOf beside a $ref at the same level lost the union outright: it reached no field of the document, was not kept under Unmodeled, and nothing was reported (#406). Under JSON Schema 2020-12 — and so OpenAPI 3.1 — $ref is an ordinary keyword whose siblings conjoin with it, so the union narrows the referenced schema; dropping it silently widens what the SDK accepts. allOf beside a $ref had the identical gap.

The union family's one keeper, preserveUnionSiblings, was reached only through the structural-body path; a declaration that peels a leading $ref never got there, and the $ref-site keyword census that already covers format/const/enum/required/additionalProperties beside a $ref (#283, #348) never named oneOf/anyOf/allOf either.

The keeper is split into a thin TypeID-addressed wrapper and preserveUnionSiblingsAt, which writes into an ir.Unmodeled map directly, and every $ref site now calls it: the component/alias position, the property carrier, and — the site the mechanism sweep turned up — a $ref inside an allOf branch (fillAllOf), which had the same gap and would have been left standing. All three write the same key form (openapi:oneOf/openapi:anyOf), reason (degraded_lowering), code and message, so a consumer sees one spelling of "a union the IR could not lower". allOf beside a $ref joins the existing unhomed-keyword census through refSiteUnhomedKeywords, confined to the $ref site: a body position already homes allOf through family election, which a $ref site never reaches, so adding it to the general censusKeywords list would misreport every ordinarily composed schema. The diagnostic message at a $ref site now states its own reason rather than the body path's ("co-declared with structural keywords…", which is false there). ir-design.md's OpenAPI row records the rule.

Test plan

  • New conformance case union-beside-ref (witnesses untagged-unions): Base, G (oneOf), H (anyOf), I (allOf) and a property carrier; each pins the alias node, Base.Target, the kept value (JSONEq), the reason, and exactly one info degraded-construct with the pinned message at the position.
  • allof-ref-branch-siblings gains UnionBranch and NestedAllOfBranch for the allOf-branch site, asserted at the branch pointer; TestRefSiteKeywords_AllOfBranchKeepsUnion covers anyOf there too. A $ref+oneOf branch under a distributed union was probed: reached once per variant, idempotent, one diagnostic.
  • Planting the defect back (reverting each site) reddens the named assertions.
  • make gate passes: lint 0 issues, coverage at 100%, fuzz, bench-smoke.

Closes #406

🤖 Generated with Claude Code

https://claude.ai/code/session_01TJZJ8HaugGZTjzVueafLAT

fuad-daoud and others added 2 commits September 11, 2026 13:47
A schema that writes oneOf/anyOf beside a $ref at the same level lost the
union outright: it reached no field of the document, was not kept under
Unmodeled, and nothing was reported. Under JSON Schema 2020-12 (and so
OpenAPI 3.1), $ref is an ordinary keyword and its siblings conjoin with
it, so the union narrows the referenced schema — dropping it silently
widens what the SDK accepts. allOf beside a $ref at the same level had
the identical gap.

The union family's one keeper, preserveUnionSiblings, was reached only
through the structural-body path (lowerBesideUnmodeledUnion); a
declaration that peels a leading $ref never reached it. The $ref-site
keyword census (unhomedKeywords) that already covers format/const/enum/
required/additionalProperties beside a $ref (#283, #348) never named
oneOf/anyOf/allOf either.

Split preserveUnionSiblings into a thin TypeID-addressed wrapper and
preserveUnionSiblingsAt, which writes directly into an ir.Unmodeled map;
refSiteRef and PreserveRefSiteKeywords now call the latter directly when
a $ref site (or a carrier's $ref) co-declares oneOf/anyOf, reusing the
structural-body path's own keeper rather than adding a second one. allOf
beside a $ref joins the existing unhomed-keyword census through a new
refSiteUnhomedKeywords helper, confined to the $ref site: a body position
already homes allOf through family election (dispatchOf/
recordSkippedFamilies), a mechanism a $ref site never reaches, so adding
allOf to the general censusKeywords list would misreport every ordinarily
composed schema as degraded.

Adds the union-beside-ref conformance case (Base/G/H plus an allOf-beside-
$ref schema, at both a component position and a property carrier) and
extends TestRefSiteKeywords_KeptAtEveryPosition/adds
TestRefSiteKeywords_UnionKeptAtEveryPosition in compilers/openapi/internal/
schema, and records the OpenAPI lowering-table row in ir-design.md.

Closes #406

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJZJ8HaugGZTjzVueafLAT
Review of the previous fix (#406) found the sweep incomplete: fillAllOf's
handling of an allOf branch spelled as a $ref is a third position with the
identical gap, alongside the component/inline-subschema and carrier sites
already fixed. `allOf: [{$ref: Base, oneOf: [...]}]` composed straight to
Base with the oneOf read by nothing — no alias, no Unmodeled, no
diagnostic. Fixed the same way as the other two sites: the branch now
routes through refSiteUnhomedKeywords (allOf) and preserveUnionSiblings
(oneOf/anyOf), reusing both keepers rather than adding a third.

Also fixes a self-contradictory diagnostic: the shared message template
asserted "co-declared with structural keywords" even at a $ref site, where
there is no structural body. The prefix now lives only in the
structural-body path's five reasons; a $ref site's own why is a complete,
non-contradictory sentence.

Minor cleanups from the same review: the Carrier position's diagnostic is
now pinned symmetrically with the other three; the duplicated
oneOf/anyOf-presence check across three call sites is one declaresUnion
predicate; and the duplicated GoDoc between censusKeywords and
refSiteUnhomedKeywords is now argued in one place.

Extends the allof-ref-branch-siblings conformance case with the branch-site
union/allOf rows and adds TestRefSiteKeywords_AllOfBranchKeepsUnion in
compilers/openapi/internal/schema for coverage the top-level conformance
test cannot provide (it runs in a different package).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJZJ8HaugGZTjzVueafLAT
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.

openapi: oneOf/anyOf beside a $ref vanishes without a diagnostic

1 participant