Skip to content

feat!: follow references into OpenAPI 3.2 components/mediaTypes - #1333

Merged
OmarAlJarrah merged 12 commits into
mainfrom
feat/openapi-component-media-types
Nov 2, 2026
Merged

OmarAlJarrah merged 12 commits into
mainfrom
feat/openapi-component-media-types

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

OpenAPI 3.2 adds components/mediaTypes and lets a content entry be a Reference Object to one of its entries. The parser models neither, so the compiler never followed such a reference: the entry lowered with no schema, and the $ref and the whole section were kept verbatim with a warning. This follows them, designed against the four defects #856 records of a first attempt.

  • Resolved where every reference is. The load phase models each components/mediaTypes entry, and each content entry written as a $ref, as the parser's generic reference over a Media Type Object (resolve.ReferencedMediaType), walks them with the source, and hands them to the parser's resolver as it hands every other reference object: under the same reader, budgets and refusals, with what they reach held to the rule table. A content entry written as a $ref inside another document is followed too.
  • One declaration, one type. What a reference reaches lowers once, at the pointer declaring it, named for its components/mediaTypes key, however many content entries reach it (request and response bodies, content-style parameters and headers) and in whichever order they lower. The binder learns the section, so a schema $ref inside an entry, or into one, is bound where it is declared.
  • References inside the entry resolve. A chain ends on the model the load phase walks, not on a copy the resolver builds, so an example, an encoding header or a schema $ref inside the entry lowers with what it names.
  • Chains of entries are followed as any reference chain is, and a cycle among them is reported as one among other reference objects is.
  • Siblings beside the $ref. ir.Content gains Docs: a 3.2 Media Type Object's description, kept verbatim until now, and the summary and description beside a $ref to it, folded along the chain as every other position folds them. A content-style parameter or header, whose carrier has no docs field for its media type, keeps what the fold takes verbatim, each from the position writing it.
  • Retention. An entry nothing references joins the retained components as openapi:components/mediaTypes/<name>, and the whole-section entry and both warnings go.

Two library behaviours made this more than a new alias. Both are recorded as docs/parser-library.md#media-type-references and pinned by a canary. The library's walk panics on a reference type it has no handler for, so the load phase yields these models itself. The resolver cannot take the model's own Media Type Object for a reference, so a $ref to an inline content entry would fail. The load phase holds each of the source's Media Type Objects where the resolver looks before reading the model, and resolves the source's media type references once its walk has held them all, so such a $ref reaches its target, under $self too.

Out of scope, each filed with every site of its mechanism and stated at the code and rule that have it. The first three are shared with every other kind of reference object, and a content entry joins them by being read as one: on main such an entry lowered as written, with the warning that its $ref was not followed.

#1229 landed with IRVersion 0.27.0 and #1325 with 0.28.0, so this change takes 0.29.0. #1269 also claims 0.27.0 from an older base; whichever of the two merges later takes the next version.

Closes #856

Breaking

IRVersion moves to 0.29.0, and a consumer pinned to 0.28.0 refuses the documents this version writes. Content gains docs. A Media Type Object's description moves from Content.Unmodeled["openapi:description"] to Content.Docs.Description; a content entry written as a $ref lowers what it names and no longer carries openapi:$ref; and Document.Unmodeled["openapi:components/mediaTypes"] gives way to one entry per entry nothing references.

Test plan

  • operation: one declaration lowers to one type in both declaration orders, for a component of the source and of another document and for an inline content entry, with and without a reader; references inside the declaration resolve; docs fold along an alias chain at a body and at a parameter, and one an alias in another document writes is reported; a body's Content.Docs and what a parameter keeps agree over every spelling of each key (left out, empty, null, text) at the use site, the alias and the declaration; a parameter and a header naming a media type in another document read its schema there, placing what they keep verbatim in that document and leaving a bound on the schema's own node; an unresolved reference lowers nothing; a $ref holding no string draws one finding.
  • load: the section's entries are modelled as the parser reads it; the source's walk yields each model once; a chain ends on the declaration's own model, by the key the resolver uses, under $self, and through a spelling that key misses; a fault building a reference is reported once, in whichever document writes it; what a reference reaches in another document is judged by the rule table.
  • e2e: a source rebuilt for an anchored document it reads lowers a reference into that document as a source not rebuilt does.
  • canary: TestLibraryBehaviour/media-type-references.
  • Conformance: component-media-types-32 rewritten to exercise the feature; the later-version-constructs and unreferenced-components goldens regenerated.
  • Each guard was mutated and the tests above went red: the cache hold, the deferred resolution, the canonical model, the census key, the binder's section and reference kind, the docs fold, the unset reading, the declaration's summary, the naming, the retention, the fault report, the reached object's kind, the rebuild's modelling, and the context each content-style carrier reads its schema in, at all four calls.
  • The golden spec (cloudflare/api-schemas @319dc756) compiles to byte-identical IR and diagnostics against main, irVersion aside, as do the five OpenAPI 3.2 specs of a real-world spec corpus. Over every spec under testdata and that corpus, 2,181 in all, only the three conformance cases this change rewrites differ from main, with the same exit codes (9c3065c against 02c972b).
  • After merging feat(ir)!: type the secrecy and stability a document states #1229: a parameter and a header whose content $ref names a media type in another document, whose schema is deprecated and x-sensitive, are marked as ones naming that schema directly are.
  • Cost, measured at a load average near 60. On a 3.7 MB OpenAPI 3.0 spec using no media type reference, CPU profiles put about 0.25% of compile time in the new walk step, and the functions this change adds allocate under 1% of the bytes. Instructions retired, one thread with the collector off, read 1.2% to 1.6% above main there; the largest difference between the two builds' profiles is the runtime returning memory to the OS rather than compiler code, and such counts include that work, which moves with the machine's memory pressure. Content references scale linearly, about 3.95M instructions per operation of three references from 500 to 4,000 operations, and cost under a quarter of the same entries written inline, since a declaration lowers once.
  • CI runs the gate.

OpenAPI 3.2 adds components/mediaTypes and lets a content entry be a
Reference Object to one of its entries. The parser models neither, so
the reference was never followed: the entry lowered with no schema,
and the $ref and the section were kept verbatim with a warning.

The load phase now models each components/mediaTypes entry and each
content entry written as a $ref as a reference object of the parser's
generic kind, walks them with the source, and resolves them through
the parser's resolver as it resolves every other reference object,
under the same reader, budgets and validation. A declaration lowers
once, at its own pointer, however many content entries reach it, with
the references it holds resolved where it is declared, and an entry
nothing references joins the retained components.

ir.Content gains Docs, which holds a media type's own description and
the summary and description written beside a $ref to it. IRVersion
moves to 0.27.0, and a consumer pinned to 0.26.0 refuses the documents
this version writes.
A media type walk's doc comment ran one word past the cap, and a test
built a slice it could preallocate.
A summary or description written empty or null beside a content entry's
$ref reads as unset in a body's Content.Docs, so the declaration's own
description stands. A content-style parameter or header, which keeps
what that fold takes verbatim, took the empty value instead and dropped
the declaration's. It now skips an unset value as the fold does.

The entry's own context and pointer on mediaDecl are named for what
they hold, keepContentDocs returns its keys before its diagnostics as
its neighbours do, and the fault reporter is named for what it does.
Three paths of the media type references had no test that a defect in
them would redden. A parameter's kept docs and a body's Content.Docs
could read a key apart, as an empty sibling did before the last commit;
what a reference reaches in another document could go unjudged by the
rule table; and a source rebuilt for an anchored document it reads
could lower a reference into that document at its use site instead of
at its declaration.

One table test now holds the two docs readers to one answer over every
spelling at the use site, the alias and the declaration. The test of
each kind of reached object gains a media type, and an end-to-end test
compares the IR of a rebuilt source with that of one not rebuilt.
A content entry written as a $ref lowers nothing, and nothing reports
it, where it sits inside an object reached in raw YAML (#1320) or its
$ref decodes as no string (#1280); the keys beside the $ref other than
summary and description are not read (#1321). declaredMedia said the
load phase or the rule table always reports why, and the rules named
neither #1280 nor #1321. Both now state each limitation.

walkSource no longer claims a document with no media type reference is
walked as the library walks it: every walk yields each Media Type
Object's model after it. The content rule now says a schema is named
where its media type is declared, after its key only when that is in
components/mediaTypes.
A schema $ref whose pointer passes through a reference object's alias
lowers what it reaches a second time, at the pointer it spells, beside
the node of the declaration itself. Every kind such a pointer can pass
does this on main; a content entry's alias into components/mediaTypes
now does too, where main reported that $ref unresolved. The references
rule now states it, and #1359 tracks the fix.
#1229 took IRVersion 0.27.0, so this change moves to 0.28.0: main's row
and history entry stand, and this change's follow under the new number,
with the shape hash of the merged tree. fillParamType returns the
elected spelling, as #1229 has it, and reads its schema in the context
declaring it, as this change has it.
A parameter or header whose content entry names a Media Type Object in
another document reads that object's schema in the document declaring
it. Two of the four calls doing so had no test: reading the parameter's
schema in the entry's context placed what the parameter keeps verbatim
in the wrong document, and filling the header's detail there copied a
bound the schema's own node holds onto the header as well. The merge of
main had to choose the first of these, between its own call and this
change's.

A test now compiles both carriers against another document's
declarations and asserts where each kept key is placed and that a bound
stays on the node alone.
At a parameter's or header's content entry, the summary the fold takes
from the use site or an alias is kept under the entry's own Unmodeled
key, and so is a summary the media type's declaration writes, which a
Media Type Object does not define. When both are written, the census
reports the declaration's as represented in no form. A body keeps both,
one in Content.Docs and one in Content.Unmodeled.

This is the namespace a carrier shares with what it lowers, which #396
tracks; keepContentDocs and the reference-object-siblings rule now say
so.
contentOf looked its path up only to print the path item it found in a
failure message, which renders as no path; response200 already checks
the path, so contentOf now prints the path and media type it was asked
for. A rules comment in internal/archtest ran past the wrap its
neighbours keep.
#1325 took IRVersion 0.28.0, a change of meaning over 0.27.0's shape,
so this change moves to 0.29.0: main's rows and history entries stand,
and this change's follow under the new number with the same shape hash
as before, since #1325 changed no shape.
@OmarAlJarrah
OmarAlJarrah merged commit 737ffb5 into main Nov 2, 2026
4 checks passed
@OmarAlJarrah
OmarAlJarrah deleted the feat/openapi-component-media-types branch November 2, 2026 13:00
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
main took IRVersion 0.29.0 for Content.Docs (#1333), so the renamed path
item doc keys move to 0.30.0: a row after 0.29.0's with its new shape and
the same names, and a history entry naming what a consumer pinned to
0.29.0 sees.
OmarAlJarrah added a commit that referenced this pull request Nov 2, 2026
Brings in #1322 and #1333. Both merge clean but for the version.

#1333 took IRVersion 0.29.0, gaining Content.Docs, so this branch's
bump moves to 0.30.0. Its row takes #1333's shape and keeps this
branch's names and meaning, and its version history entry moves with
it, pinned against 0.29.0.
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: resolve OpenAPI 3.2 components/mediaTypes references

1 participant