Skip to content

fix(downgrader): keep a $ref beside only annotations bare in a tagged union in 3.0 - #150

Merged
dinwwwh merged 1 commit into
mainfrom
claude/project-thread-llvchi
Oct 28, 2026
Merged

dinwwwh merged 1 commit into
mainfrom
claude/project-thread-llvchi

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 28, 2026

Copy link
Copy Markdown
Member

Requested by dinwwwh · project thread

Problem

Real 3.1 documents give each branch of a tagged union its own description:

{
  "oneOf": [
    { "$ref": "#/components/schemas/RequestTextBlock", "description": "Regular text content." },
    { "$ref": "#/components/schemas/RequestImageBlock", "description": "Image content ..." }
  ],
  "discriminator": { "propertyName": "type", "mapping": { "text": "#/components/schemas/RequestTextBlock", "image": "#/components/schemas/RequestImageBlock" } }
}

A $ref with siblings becomes { description, allOf: [{ $ref }] } in 3.0, so every branch turned into an inline schema. OpenAPI 3.0.4 says that beside a discriminator "all possible schemas MUST be listed explicitly", and that "inline oneOf or anyOf subschemas are not considered" for the mapping. Validation was unchanged, but the discriminator stopped describing the union. oapi-codegen 2.8.1 generates from 3.1 but stops on our 3.0 with ambiguous discriminator.mapping: please replace inlined object with $ref for the Anthropic and ElevenLabs API documents.

Fix

In a oneOf or anyOf beside a discriminator that stays in 3.0, a branch that is a $ref beside only annotations (description, title, deprecated, default, example, readOnly, writeOnly, externalDocs, xml, x-*) is written as the bare $ref. 3.0 ignores the siblings of a $ref anyway. Every other $ref with siblings keeps the allOf wrapper, including a branch with a non-annotation sibling such as type, and branches beside a discriminator that is removed because a branch points into $defs (Pydantic, msgspec), so they keep their descriptions there.

The existing $defs discriminator check moves into keepsDiscriminator, which the oneOf/anyOf fields and finishSchema share.

README: the "A $ref with siblings" row says so, and the Kiota bullet from #145 now leaves out the branches this keeps bare, which Kiota reads fine.

Checks

  • Output on real documents, main vs this branch: the Anthropic API spec changes at 16 branches (description), ElevenLabs 66 (description), OpenAI 2.3.0 4 (title, deprecated). Nothing else changes. Azure OpenAI, Cohere, Mistral, Perplexity, Jina and others are unchanged. Validator errors (@seriousme/openapi-schema-validator) are identical to main's for all three.
  • oapi-codegen 2.8.1 (models + client) on the 3.0 output: Anthropic and ElevenLabs fail on main and generate with this branch.
  • Perf (in-process, gc before each run, shuffled, twin builds, median ms): api100 12.46/12.81 main vs 12.91/12.32 new, path item chain 0.80/0.75 vs 0.78/0.76, Anthropic 26.8/26.5 vs 26.9/26.1, ElevenLabs 54.9/56.3 vs 55.9/54.4, OpenAI 32.5/30.2 vs 30.5/32.0. All within twin noise.
  • pnpm run check clean, 286 tests pass, coverage 100%, snapshots unchanged.

Generated by Claude Code

… union in 3.0

A `oneOf` or `anyOf` branch that is a `$ref` beside a `description`, `title` or other annotation became `{ description, allOf: [{ $ref }] }` in 3.0, as every `$ref` with siblings does. Beside a `discriminator`, OpenAPI 3.0 maps values only to the `$ref`s among the branches, not to inline schemas, so the discriminator stopped describing the union, and oapi-codegen 2.8.1 stopped with "ambiguous discriminator.mapping" on the Anthropic and ElevenLabs API documents. Such a branch now stays the bare `$ref`, whose siblings 3.0 ignores, when the discriminator stays. Every other `$ref` with siblings keeps the `allOf`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7PJZWFiNWco7tWuntN
@dinwwwh dinwwwh self-assigned this Oct 28, 2026
@codecov

codecov Bot commented Oct 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Oct 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 21 untouched benchmarks


Comparing claude/project-thread-llvchi (cd76c2f) with main (9f9e19b)

Open in CodSpeed

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one minor suggestion inline.

Reviewed changes

  • Tagged-union branch conversion — new convertBranches/convertTaggedBranch write a branch that is a $ref beside only annotations, in a oneOf/anyOf beside a retained discriminator, as the bare $ref rather than { allOf: [{ $ref }] }, so 3.0's discriminator maps it again (OAS: "inline schemas will not be considered"). Verified the premise against the 3.1.0 spec.
  • keepsDiscriminator extraction — the $defs discriminator-removal check is shared between the anyOf/oneOf field converters and finishSchema; the refactor is logically identical to the previous inline condition (out.discriminator !== undefined implies schema.discriminator !== undefined, so the delete condition reduces to the same expression).
  • Tests + README — covers oneOf/anyOf, the no-discriminator wrap, and the $defs-removed case; README row and Kiota bullet updated to match.

Local verification: vitest run passes 286/286, uncheck (oxlint/oxfmt/tsc) clean on the changed files, snapshots unchanged. The only observation is the input-key-based annotation check below — non-blocking.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread packages/downgrader/src/v3.1-to-v3.0.ts
@dinwwwh
dinwwwh merged commit a9ea06d into main Oct 28, 2026
11 checks passed
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.

2 participants