fix(compilers/openapi): keep keys the source model does not name - #356
Open
OmarAlJarrah wants to merge 1 commit into
Open
fix(compilers/openapi): keep keys the source model does not name#356OmarAlJarrah wants to merge 1 commit into
OmarAlJarrah wants to merge 1 commit into
Conversation
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A key the parsed OpenAPI model has no field for reached neither an IR field, an
Unmodeledentry, nor a diagnostic. Twelve object types were probed and alltwelve dropped it in silence, so two documents differing only in such a key
compiled to the same IR — a losslessness failure (invariant 2), and the reason a
misspelt
operationIdsurvives review.The parser already takes the census: every core model records the keys it did not
name. Nothing read it. This adds one shared reader over that census and calls it
at each object's lowering, keying entries the way the rest of the compiler does —
unscoped on an object that lowers to a node with an
Unmodeledmap of its own,and scoped by the source path from the carrier down for the objects that ride on
one (
openapi:info/contact/slack,openapi:tags/0/color; tags by index, notname, since two tags spelled alike would otherwise leave one entry).
The two vocabularies are graded apart, because the specifications differ:
requires implementations to ignore what they do not recognize, and allows such a
keyword to carry meaning for other tooling. Kept, and announced at info
under
openapi/unknown-schema-keyword: a recorded decision, not a fault.x-, so a key that isneither defined nor prefixed is invalid input. Kept all the same, and announced
at warning under
openapi/unknown-object-key.Warning rather than error for the reason
ReservedHeaderNameis one: the documentstill lowers, and
harness.Checkstops at the first error diagnostic, which wouldhide every later finding in the file and leave any fixture carrying a stray key
unable to reach the invariant checks.
Both carry
ReasonOutOfScope. That is a property of the construct — no IR node iscoming for either — while which of the two a key is says something about the
document, and the diagnostic channel is where that belongs.
Three further points:
names. It runs last at each position and leaves alone any key already
recorded, so
$vocabularyanddependentRequiredkeep the reasons their ownreaders give them. Keywords no reader leaves a trace of are named in
annotation.DecidedKeywords:$commentand$dynamicAnchorare deliberatedrops recorded in the 2020-12 vocabulary table, and an expanded
$dynamicRefmust not also be preserved.
MaxUnknownKeysper object, with the remainder reported underopenapi/unknown-key-budgetrather than dropped in silence. The census is alsosorted on a copy before use: the library builds it from a parallel walk, so its
order is neither source order nor stable.
allof-oneof-cooccurrence.yamlsplitdescription: named by position, not by targetinto a description and a straynot by targetkey; the description isnow quoted so the fixture says what it meant.
Boundary with the neighbouring work
This is keys the source model does not name at all. Keys the model does name
which the elected lowering never reads are a different gap with a different fix
(#268, #283), and the census cannot see them by construction. Vendor
x-*extensions are out of scope here and covered by #345.
Not swept, and why
Path items are excluded: the library folds an unrecognized key into the path
item's embedded operations map and reports a type mismatch instead, so there is no
census to read.
paths,responses,componentsand callbacks are maps whereevery key is a valid entry, so "unknown" is undefined for them. The remaining
objects — example, link, encoding, the OAuth flows, discriminator, xml, and
externalDocsbelow the document root — lower to no node that holds anUnmodeledmap today; the carriers for them are what #345 is adding, so theircensus belongs on top of that rather than duplicating its scaffolding. Filed as
#357.
Test plan
testdata/openapi/unknown_keys.yamlwrites one undeclared key at each of theeighteen positions, valued with the object it was written on. Living under
testdata/is what runs it through all six oracles — including the two-orderdiff and the round-trip — rather than through assertions alone.
TestUnknownKeys_KeptAtEveryObjectfinds each entry by walking the document'svalue graph and matching on key and value, so an entry written to the wrong
carrier fails rather than passing because the assertion looked only where it
expected. Every row was red before the change; reverting the lowering with the
test kept turns all eighteen red again.
TestUnknownKeys_SchemaAndObjectAreGradedApartpins the info/warning split andthe provenance of both.
TestUnknownKeys_WellFormedDocumentRecordsNothingis the control: a documentwriting only what the model names keeps nothing and reports nothing.
annotationcover the sort, the bound, the already-recorded skip,the decided-keyword skip, an unconvertible value, and the shapes the reader must
survive rather than panic on.
Closes #297