Repository navigation
fix(compilers/openapi): read a schema in the dialect it names - #1459
Merged
Merged
Conversation
OpenAPI 3.0 ignores the keywords written beside a schema's $ref. The compiler applies them as 3.1 does and warns of each, but then read each keyword in a different dialect: `nullable` as JSON Schema 2020-12 reads it, which defines no such keyword, `exclusiveMinimum` as 3.0 reads it, and 3.0's requirement of `items` beside `type: array` not at all. The warning claimed a 3.1 reading, which was false for `exclusiveMinimum`. Keywords beside a 3.0 $ref are now read as 3.0 defines them, the only reading under which what the author wrote means what it says: a `nullable` there adds null to a `type` written beside it, in the null reading and in what admission reads a schema to admit; with the policy off it is no undefined keyword, so it is not kept verbatim; and a `type: array` there with no `items` draws openapi/missing-array-items. The warning now says the keyword is applied as in 3.1 and read as in 3.0. The reason openapi/inferred-nullable gives for a nullable beside a $ref no longer says no version gives it an effect. A 3.1 one names the dialect, a 3.0 one with no type beside it says it writes none, and one whose null the conjunction removes names what removed it: an allOf beside it, or what the $ref lands on.
The rule table in internal/oas states, per minor, each fact the schema lowering branches on: that `nullable` is a Schema Object field only in 3.0, that 3.0's exclusive bounds are booleans and 2020-12's numbers, and that 3.0 requires `items` beside `type: array`. The lowering read none of those rows. It asked lowering.Ctx.Is30, which parsed the document's version, so each fact was stated twice, in two packages, with nothing holding the two to each other. oas.SchemaRules is the Schema Object's rows at one minor, built once when a context is made, and Ctx.SchemaRules replaces Is30. The lowering now asks whether the dialect defines `nullable`, what shape `exclusiveMinimum` takes, and whether a schema whose `type` is `array` must write `items`: the rule's condition is read from its row too, so the lowering's test of `type` is the table's. SchemaRules has no accessor for its minor, so a holder of one cannot branch on it, and a version the table does not cover answers with rows defining nothing, as Is30 answered false. The one output change is the reason openapi/inferred-nullable gives in a 2020-12 schema, which now says the dialect it is read in defines no nullable keyword rather than quoting the declared version.
A document a reference reaches beside the source was read at the source's minor, whatever it declared. A 3.0 root reaching a 3.1 response refused the response's numeric exclusiveMinimum, a 3.1 root reaching a 3.2 response required the description 3.2 dropped, a 3.0 schema reached from 3.1 lost its nullable and its required items, and Document.Sources recorded every such document as read at the source's minor. Each document is now read at the minor it declares: its schemas in that minor's dialect, its reached objects held to that minor's table, and its Document.Sources entry naming it. A document declaring no version, such as a bare schema file, or one the table does not cover, such as 3.3.0 or a Swagger 2.0 file, is read at the source's minor as before, and the compile still stands. load.Other carries the Schema Object's rows at its minor, and lowering.Ctx.SchemaRules answers with those of the document its scope reads. The null reading crosses documents only at a $ref, so each alias and the end of a chain are read in their own document's dialect, and the walk returns to its own once it has read them. What admission reads crosses documents through declaration sets as well, so it reads each schema in the dialect of the document the bindings place it in. The load phase now builds the rule table once and shares it between judging the source, judging what references reach, and reading each document's rows, where it built the table twice before.
That no lowering asks the OpenAPI version a document declares, and reads what a construct means at that minor from the rule table instead, was a sentence in the lowering specification that nothing enforced. A lowering that read the version again would state a fact the table already states, and nothing would hold the two to each other. The versionreadings analyzer, which make vet runs, refuses every reading of the version outside compilers/openapi/internal/load and compilers/openapi/internal/oas, in production files: the parser library's version field and GetOpenAPI method of its document type, promoted or not, oas.ParseMinor and load.SupportedMinor. The library names other fields OpenAPI too, a walk's among them, so the field is the one its document type declares, compared as an object rather than by name. Detection's own probe of a source's version is no use of the library's type, so routing a source by its version stays allowed. The plants cover each form a use takes, the two exempt packages, a look-alike type, a test file and a package outside the module.
Two sites the change reads its dialect at had no test that told a wrong reading from the right one. The agreement of an allOf's value bounds reads each conjunct in its document's dialect. A numeric exclusiveMinimum is a bound from 3.1 on, so two writers of one meet and fold; in 3.0 the lowering reports and drops it, so its writers state nothing and stay apart. The schema readers ask the rows of a schema's document for every schema they read across the bindings, most often the source's. Those come without a lookup by name, which would parse the name as a URL on each miss; an allocation count holds them to it.
Reading each schema in its own document's dialect gave admission two methods, past the 20 internal/archtest allows a type under compilers/. Neither needed the walker's state. Which dialect a schema is read in follows from the context and where the bindings place the schema, so it is a function of those, definesNullableAt. What a schema writes takes that dialect as an argument, so each reader says which one it reads in: the schema's own document's across the bindings, c's for a schema of c's document.
The document-minor table carried a check function per case, which rule 6.2 of the styleguide reads as a red flag; each case now states the bounds and the missing-items warnings it expects. The helper takes the nullable-widening policy rather than a bare bool, the two-minor test preallocates what it collects, an allocation count reads its answer outside the counted function, and a comparison of rows holding an unexported field compares their identity.
testifylint refuses assert.True over a comparison and asks for assert.Equal, but assert.Equal compares deeply, so rows built twice would pass it. The claim is that a minor's rows are built once, so the test checks identity itself and reports a miss with t.Error.
Three places still said the keywords written beside a 3.0 schema's $ref are read as 3.1 reads them, which stopped being true when they began to be read as 3.0 defines them: the IR's unmodeled-content chapter, the comment on the Schema Object's row in the rule table, and the reason the validation canary gives for its ref-siblings class. Each now says they are applied as 3.1 applies them and read as 3.0 defines them. The nullable rule's paragraph is rewrapped where an earlier edit left a long line.
OpenAPI 3.1 has a root Schema Object's `$schema` choose the JSON Schema dialect it is read in, and a document's jsonSchemaDialect stand in for it where none is written. Both were kept verbatim and ignored, so a draft 4 schema's `minimum: 5, exclusiveMinimum: true`, which means x > 5, was refused as a malformed 3.1 bound. The rule table now has rows by dialect as well as by minor: the OAS dialect and JSON Schema 2020-12 read as the minor's own keywords, and draft 4's keywords stand in for them where a schema names it. A dialect changes the keywords alone; the fields and rules OpenAPI adds to a Schema Object stay the minor's. The binder notes the `$schema` each schema no other holds writes, a document of schemas naming its dialect at its root, and the load phase hands the lowering the rows each such root governs by the nodes of its tree, with each document's jsonSchemaDialect as the default. The exclusive-bound reading asks the rows of the schema it reads by its node, so a schema a `$ref` names is read in its own root's dialect whichever declaration lowers first. A dialect the table has no rows for, a later draft or one of the author's, is read as the minor's own, with an openapi/unread-dialect warning at the `$schema` or jsonSchemaDialect naming it. A 3.0 document defines neither keyword, so nothing changes there.
A draft before 2020-12 still has the keywords it spells otherwise read as 2020-12 reads them, the table has no rows for drafts 6, 7 and 2019-09, and a $schema at the root of a resource an $id embeds chooses nothing. The dialect rule's Status and the code that draws each line now name the issue tracking it.
A `$schema` or jsonSchemaDialect that is no absolute URI, such as a number or a relative path, is already an openapi/validation finding of the rule table, which formats both rows as absolute URIs. It also drew an openapi/unread-dialect warning at the same place, two findings for one defect. SchemaRules now reports a dialect as unread only where an absolute URI names one the table has no rows for; a malformed value reads as the minor's own, as before, and is reported once. The dialect rule also states what a node an alias places at several positions reads: the dialect of the first root whose tree holds it, at every position. No reading today tells one valid node's two dialects apart, since a valid exclusive bound in one is malformed in the other.
The parser library holds a 3.1 schema to its version's 2020-12 meta-schema whatever dialect the schema names, so it refuses the new conformance case's draft 4 bounds; the validation oracle now gives that disagreement a reason of its own. The annotation package's own tests read the info of each dialect keyword, `$vocabulary`'s included, which only the end-to-end case reached, and the new tests are written as the linters ask.
openapi/unread-dialect joins the diag package's list of every code it declares, and the load phase spells the jsonSchemaDialect pointer itself rather than reaching the ids package, which its layer may not import.
OpenAPI 3.1.1 has a `$schema` choose the dialect of any Schema Object that is a schema resource root, and JSON Schema roots a resource of its own at a subschema whose $id names another URI. Only a Schema Object no other holds, or a schema file's root, chose one, so a draft 4 property with an $id beside its `$schema` was refused for its boolean bound. The binder now counts a schema whose $id roots a resource as a dialect root too, and the load phase governs roots innermost first, so a nested root keeps its own dialect for what it holds while the rest of the outer root reads the outer's. An $id naming its own document roots no resource and still chooses nothing.
…rom-table Two conflicts. internal/archtest's allowlist for lowering keeps main's nodeview and yaml with the branch's oas, and its comment says both. lowering's version test keeps the branch's assertions under main's new BinaryMedia parameter, which the branch's other New calls now pass too. main's multipart part reading (#1435) asks Ctx.Is30, which this branch removes: a part writing no type is a file from 3.1, whose Encoding Object defaults its contentType to application/octet-stream. Until the table states that default, it asks whether the dialect defines nullable, which answers the same at every minor.
lowering.New built the whole rule table a second time per compile to read the source's Schema Object rows, while the load phase already held one. The load phase now reads the source's rows off the table it judged the source against, through the dialects that reads each reached document, so a document read at the source's minor shares them, and hands them over as load.Document.Rules. A context reads them from what WithLoaded carries; one given no load phase still reads the version its document declares. A compile now builds the table once. BenchmarkCompile_Petstore, three runs each at GOMAXPROCS=1: 1,442,131 to 1,319,664 B/op and 12,664 to 12,113 allocs/op.
A multipart part writing no type is a file from OpenAPI 3.1, whose Encoding Object defaults its contentType to application/octet-stream (3.1.1 and 3.2.0, "The default values for `contentType` are as follows": a schema with type absent takes application/octet-stream; 3.0.4's table has no such row). That reading asked the version, and the merge of main kept it by asking whether the dialect defines nullable. The table now states it: a Field carries Defaults, each a value for one case at the minors its span holds, and the Encoding Object's contentType defaults a schema writing no type to application/octet-stream from 3.1. oas.EncodingRules is the Encoding Object's rows at one minor, as SchemaRules is the Schema Object's, and load.Rows pairs the two, so a document's rows, the source's included, come from one table per compile. lowering.EncodingRules reads them in the document whose content the context reads, and the part is then judged as naming that media type. The table's own tests hold each default to its citation, to minors its field is defined at, and to one value per case at each minor.
The rule that no lowering reads the version a document declares was a go/analysis analyzer of its own in internal/lintrules. forbidigo expresses it: two patterns refuse the parser library's version field and accessor, and oas.ParseMinor, outside the OpenAPI compiler's load phase and rule table, which an exclusion excuses, and outside tests, as the analyzer did. A member promoted through an embedding of the library's document is the one spelling forbidigo does not see, and nothing embeds it. A struct literal writing the version, which the analyzer refused as a read, is no use forbidigo matches. load.SupportedMinor, read nowhere outside load, is unexported, so the compiler confines it and no pattern is needed. The plants move to scripts/lint-planted.sh's tree: look-alikes of load and oas under internal/lintrules/testdata/planted read the version and draw nothing, and one of a lowering draws each refusal, a context's c.Doc.GetOpenAPI() among them. The analyzer, its tests and its plants go, and internal/lintrules and cmd/morphic-vet return to main's.
A 3.0 $ref site writing type: array and no items drew
openapi/missing-array-items, which says only the schema's other keywords
constrain its elements, even where the $ref lands on a schema writing
items: {$ref: X, type: array} over X: {type: array, items: {type:
string}} lowers to an alias over X, a list of strings, and the warning
was false. The same conjunction spelled {allOf: [{$ref: X}, {type:
array}]} drew none, as an inline allOf branch restating what a
refinement names is not reported.
The keywords beside a $ref are now held to the rule with what they
conjoin with: the site is reported only where admission's reading of
the whole site, its aliases and allOfs included, finds no items. An
items it finds holds however much of the rest it read, and a reading cut
short before reaching one leaves the site reported as written.
Three statements had drifted from what the compiler does. diag.LaterVersionConstruct said a keyword beside a 3.0 $ref is lowered as in a 3.1 document, a line after saying it is read as 3.0 defines it, and later-version-construct opened the same way. openapi-versions said a version that is not major.minor.patch is reported, which holds for the source alone: a document a reference reaches is read at the minor such a version names, with no finding, since only what a reference reaches is judged. TestDocumentMinor_EachDocumentIsReadAtTheMinorItDeclares now pins that case. TestSupportedMinor_AllocatesNothing pinned a cost by the lowering asking the version of every schema, which it no longer does: the version gate runs once per document, so the test goes. TestParseMinor_AllocatesNothing keeps its pin under the reason that remains, Check and CheckFormat asking it of every object the load phase judges.
nulls.lands reads each alias of a $ref chain, and its end, in the dialect of the document holding it, and set the read's own back by hand after the last. A return added between the two would have left the rest of the read in another document's dialect. The restore is now deferred, so it holds on every way out; the closure does not escape, and the deferred call costs no allocation.
A Default held on every Field grew each of the table's Field rows by a slice header, and the table is built once a compile: the Petstore compile allocated about 11 KB more for one default the Encoding Object states. Defaults now sit on their Object beside its Rules, each naming its field, as a rule does, and the table's tests hold each to a field its object defines at every minor the default holds at. BenchmarkCompile_Petstore, three runs each at GOMAXPROCS=1: 1,332,877 to 1,322,027 B/op, 12,118 allocs/op either way.
missing-array-items' rule said a $ref site is reported where nothing it conjoins with writes an items, while the reading behind it, admission's, holds the elements to no items for items: true, and none for an items only a union's branches write. Both still draw the warning, and the rule and missingArrayItemsBesideRef now say so; TestLower_MissingArrayItems pins each. The table's descriptions name the defaults it now states: the oas package doc, the validation rule, and openapi-versions, whose reached document's Encoding Objects take its minor's defaults. Three comments are made exact: the load phase's dialects reads the source and each reached document, not the source first; the forbidigo rule names tests among what its exclusions excuse; and the test exclusion says a test may read a fixture's version.
The one DefaultCase constant was Untyped, which beside oas's Shape constants (String, Boolean, Number) reads as one of them. The package's other enumeration names its members after its type (RuleExclusive, RuleRequiredWhen), so the case is DefaultUntyped.
forbidigo refuses an assignment to the parser library's version field as it refuses a read, with the same message. Nothing outside the load phase writes a parsed document, so that is the rule wanted; its comment now says so, beside the struct literal it does not match.
This was referenced Nov 4, 2026
This was
linked to
issues
Nov 4, 2026
The parser library models a document twice, and its core model holds the version as a field of its own: c.Doc.GetCore().OpenAPI.Value read the version past both forbidigo patterns, and past the analyzer they replaced. Most lowerings already reach core models (GetCore), so the spelling is a near one. A third pattern refuses that field outside the load phase, the table and tests, and the plants hold it: the lowering look-alike draws it, the load look-alike reads it with nothing drawn. The rule's comment now names the two spellings no pattern can hold, the version as the YAML tree or the IR's SourceInfo.Format writes it, and that nothing outside the load phase reads either.
file-bodies said a keyword beside a 3.0 $ref holds with what it names "as the schema lowering reads one", and later-version-construct, which it cites, now tells applying such a keyword from reading it: the first is what file-bodies means. later-version-construct said a type: array there needs no items where the $ref lands on one, while missing-array-items, which it cites, counts neither items: true nor an items a union's branches write; it now says "supplies", the cited rule saying what does. loadResult's comment says why copies share the source's rows and the reached documents: nothing writes either after the load, so no lowering changes what another reads.
…ed-dialect The base now hands the lowering a document's rows as load.Rows, the Schema Object's and the Encoding Object's together, read once per compile off the table the load judged against, and keeps one dialects value on the load. The dialect each schema is read in joins that: the cache keys rows by minor and dialect, the source's jsonSchemaDialect rides on the shared dialects, and load.RowsOf reads it for a lowering given no load phase. The Encoding Object's rows stay the minor's.
…dialect main gained #1450 as one squashed commit whose tree is that of the base branch's last head, merged in the commit before, so it brings nothing the branch does not already hold.
Table built JSON Schema draft 4's keyword rows on every compile, though only a schema naming that dialect reads them, and SchemaRules lost the size hint of its rows map. A Cite also grew from 40 to 56 bytes for the string naming a text other than OpenAPI's, on each of the hundreds of citations the table holds. A dialect now holds what builds its keywords, called only for the dialect a schema names, so Table builds none of them. The rows map is sized by the Schema Object's fields again, and a Cite names its text with a one-byte Text beside Minor, so it stays at 40 bytes. BenchmarkCompile_Petstore, three runs each at GOMAXPROCS=1: 1,340,205 to 1,322,455 B/op and 12,173 to 12,122 allocs/op, against main's 1,322,023 B/op and 12,118 allocs/op. Instructions retired validating testdata/golden/openapi/petstore.yaml, five runs alternating at GOMAXPROCS=1 GOGC=off, stay within noise: a median of 113.37M before (113.11M to 113.62M) and 113.21M after (112.91M to 113.35M, one run at 145.24M), against main's 113.16M (112.68M to 113.89M).
The info kept with each $schema said it "is read only to choose the dialect of the 3.1 or 3.2 schema resource it roots". A $schema below a resource root roots none and chooses nothing, nor does one in a 3.0 document, yet each drew the same words, which read as though it had been honoured. The reader cannot tell a root, so the message now states the condition: it is read only where it roots a schema resource of a 3.1 or 3.2 document, to choose that resource's dialect.
Since a resource an $id embeds names its dialect too, the comments on UnreadDialect, SchemaRulesOf, Document.Dialects and the context's reading of it, and on the load test, still spoke of a schema no other schema holds, or of any schema naming a dialect: the first misses the embedded roots the warning and the rows reach, the second takes in a $schema below a root, which governs nothing. Each now says a schema resource root, the innermost for the rows a schema is read with. dialect-keywords gave nullable as a field a dialect leaves alone, but OpenAPI adds nullable at 3.0 alone, where no dialect is chosen; it now names discriminator and xml, which OpenAPI adds at every minor.
The allOf agreement reads each conjunct's bounds in the conjunct's own dialect, and no test told that from reading them in the document's: asking the document's rows left every test green. An allOf whose branch an $id embeds as a 2020-12 resource, in a document defaulting to draft 4, now pins it: read in the document's dialect, that branch's numeric bound is refused, the branches disagree, and the allOf folds no bound. Nor did a load test pin that the source's rows are those of its own jsonSchemaDialect, which only the end-to-end tests caught. One now holds them to draft 4's, shared with a document naming the same dialect and apart from one naming none.
The allOf agreement discards what its bound reading reports, on the premise that the lowering reports it where it reads the branch. A branch kept apart is never read there, so a bound that reads as none beside one rendered otherwise goes unreported, as in 3.0 and 3.1, and now in a draft 4 schema writing numeric bounds. The comment says so and cites the issue that tracks it.
A 3.2 document naming its own dialect by the id the OpenAPI Initiative publishes it under, https://spec.openapis.org/oas/3.2/dialect/2025-09-17, drew an openapi/unread-dialect warning, as did 3.1's dated ids, though each reads as the minor's own: the table named the OAS dialect only as .../oas/3.1/dialect/base, the id OpenAPI's text gives. Each published meta-schema declares the OAS base vocabulary over 2020-12's, so the table now names the OAS dialect by its dated ids as well, as OpenAPI lets tooling support values of $schema besides: 2024-10-25 and 2024-11-10 for 3.1, and 2025-09-17 and 2026-02-26 for 3.2.
A $schema no absolute URI names, a number, an empty string or a relative path, rooted its resource in the minor's own dialect, while one written as a mapping, a list or null rooted nothing and left the schema in its document's. In a document whose jsonSchemaDialect is draft 4, the first kind read a boolean bound as 2020-12 refuses it, so the malformed $schema drew a second error, at the bound, beside the finding validation makes of the $schema itself. A value naming no dialect now chooses nothing, as though it were not written (oas.NamesDialect, the test SchemaRules already made), so the bound reads in the document's dialect and validation's finding stands alone. The rule and the comments say where validation makes that finding: where it judges the value, which in a document a reference reaches is only what the reference reaches, so a malformed jsonSchemaDialect there is reported nowhere, as a malformed version is.
dialect-keywords said a node an alias places at several positions is read in the dialect of one root whose tree holds it. The load phase governs each document's roots in reverse pointer order, the first to reach a node keeping it, so the root is the last in pointer order among those whose trees hold the node, even where a deeper one holds its anchor; for a node no alias places that is the innermost. The rule and load.Dialects.Of now say so. The rule's status now also names definitions among the keywords a draft spells otherwise, kept verbatim where 2020-12's $defs is read, and #1466, under which the rule table's check holds a schema read in a draft to its minor's keyword rows.
The architecture's account of the rule table said a version's readings are keyed by the minors that define them, and the oas package doc listed fields, rules and defaults; a schema's keywords are now also keyed by the dialect it is read in, and the table states the dialects a Schema Object can name. Both now say so. validation held every schema to its minor's rows and said nothing of a schema read in a draft, which #1466 tracks; it now has a status naming it. dialect-keywords' status also names #863: an $id that names its document by the 3.2 $self is read as rooting a resource of its own, as the binder knows a document by its location alone, so a $schema there chooses a dialect the rule says it does not.
The binder's test for a dialect root broke its condition across two lines, which rule 1.8 replaces with operands on one; it now reads whether a $schema is written, then, only where it is and no $id roots a resource there, whether a schema holds the position, so the pointer is still walked only for a schema writing one. rowsAt and dialectKeywords returned a bare bool beside their rows, which rule 11.4 names: both now call it unread. load.Dialects, which every copy of a lowering context shares, now says nothing writes it once the load phase returns it, as rule 11.12 asks of an exported type.
bind.Document.Dialects lists a document's roots in pointer order, so a root holding another precedes it and the load phase governs the inner first. No test held the order: the binder's tests compare roots as a map, and the load test nesting two roots passed about half the runs with the sort removed. A test declaring six roots out of order now holds it, failing in each of 20 runs with the list left unsorted.
OmarAlJarrah
added a commit
that referenced
this pull request
Nov 4, 2026
#1459 landed on main as dfd9fe0, whose tree is 149fd7f's, merged here. Its later rounds met this branch where both read a dialect: - The rule table builds the drafts' keyword rows only for a dialect a schema names, as #1459 builds draft 4's, and cites each draft's texts by a Text constant beside draft 4's. dialectOf returns the whole dialect, its keywords and whether it ignores what is beside a $ref. - load.Dialects keeps its rows by root and by document beside #1459's by node; readDialects reads the source's dialect off the dialects cache and the rows as load.Rows, as #1459 has them. - The comments of LaterVersionConstruct, dialectRead, SchemaRules, Dialects.Of and the dialect-keywords rule take both sides' words. LaterVersionConstruct no longer says its keyword makes the document invalid: a draft ignoring the keywords beside a $ref allows them. - A test's lowering.New call takes #1435's BinaryMedia, and the schema-drafts golden #1459's wording of the $schema info. Not carried: #1459's load phase passes over a $schema naming no dialect, while this branch's binder reads that schema's identifier and tuples in the minor's own dialect. The next commit moves the skip into the binder.
OmarAlJarrah
added a commit
that referenced
this pull request
Nov 4, 2026
A $schema no absolute URI names chooses no dialect, as though it were not written (#1459). The load phase passed over such a root, but the binder read the schema's content in the dialect the value names, which the table reads as the minor's own. In a document defaulting to draft 4, the binder then declared no `id` and bound no tuple's entries, while the lowering read both in draft 4: a $ref naming the `id` went unresolved, and was fetched where external references are allowed. The binder now asks the configuration whether a value names a dialect (bind.DialectRule.NamesNone, from oas.NamesDialect), and a $schema naming none roots nothing, so the load phase meets no such root.
OmarAlJarrah
added a commit
that referenced
this pull request
Nov 4, 2026
…ft-keywords main's tree at dfd9fe0, #1459's squash, is the tree of the head of fix/openapi-declared-dialect the previous merge brought in, so what main adds is #1335's docs/emitters alone. The conflicts the two histories draw, both carrying #1459's changes from 71e08b6, resolve to this branch's side: outside docs/emitters the merge's tree is the branch's, and inside it main's.
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
OpenAPI 3.1 has a root Schema Object's
$schemachoose the JSON Schema dialect the schema is read in, and a document'sjsonSchemaDialectstand in for it where none is written. The compiler kept both verbatim and ignored them, so a draft 4 schema was refused for a bound that is valid as written:Before:
error openapi/invalid-exclusive-bound … exclusiveMinimum must be a number in this OpenAPI dialect, exit 1. NowTlowers withexclusiveMin: "5"(x > 5), and the compile stands.What changes:
oas).SchemaRules(version, dialect)returns the Schema Object's rows at a minor in a dialect, and whether the dialect is an absolute URI the table has no rows for. The OAS dialect (https://spec.openapis.org/oas/3.1/dialect/base, and the dated ids its meta-schema is published under:…/oas/3.1/dialect/2024-10-25and2024-11-10,…/oas/3.2/dialect/2025-09-17and2026-02-26) and JSON Schema 2020-12 read as the minor's own keywords. JSON Schema draft 4 gets its keyword rows, each citing its sentence in the draft 4 texts (Cite.Text, one byte besideMinor, names a text other than OpenAPI's). They are built only for a dialect a schema names, so a compile naming none pays for none of them. A dialect id matches with or without its trailing empty fragment. A dialect swaps the keywords alone: the fields OpenAPI adds to a Schema Object, such asdiscriminatorandxml, and the rules it states of one stay the minor's, whichoas.TestSchemaRules_ADialectNamesItsKeywordsAlonepins. Thenullableand required-itemsreadings are 3.0's, where no dialect is chosen, so they ask the document's rows. At 3.0, which defines neither keyword, the dialect is ignored.bind). In the walk it already makes for$idand the anchors, it records the$schemaof each schema resource root (bind.Document.Dialects): a Schema Object no other Schema Object holds, the root of a document that is no OpenAPI document (such as a bare JSON Schema file a$refreaches), or a schema whose$idroots a resource of its own, as OpenAPI 3.1.1 has any "schema resource root" choose its dialect. A$schemaon any other schema chooses nothing, since JSON Schema writes one at a resource's root, and an$idnaming its own document roots no resource. The document is known by its location: an$idnaming it by a 3.2$selfroots one (openapi: $self is the base URI for some references and not others #863).load.Dialects), roots governed innermost first so a nested resource keeps its own dialect. Each document's default comes from its ownjsonSchemaDialect. A dialect an absolute URI names that the table has no rows for (a later draft, or one of the author's) is read as the minor's own, with a newopenapi/unread-dialectwarning at the$schemaorjsonSchemaDialectnaming it. A value that is no absolute URI, such as a number, an empty string or a relative path, chooses nothing, as though it were not written (oas.NamesDialect): it stays the rule table'sopenapi/validationfinding alone, where the table judges it, and draws no second report, so a bound beside it reads in its document's dialect.$ref's siblings, the lookup has to move to positions; that requirement is recorded on openapi: keywords a draft before 2020-12 spells otherwise are read as 2020-12 reads them #1457.lowering.SchemaRulesOf), inconstraintsAtand in the allOf agreement. It does not ask the context the reader happens to hold. So a schema a$refnames is read in its own root's dialect, whichever declaration lowers first, whiche2e.TestDocumentDialect_AReferenceReadsWhatItNamesInItsRootsDialectdiffs across both orders.$schemainfo, which says it is read only where it roots a schema resource of a 3.1 or 3.2 document, as a$schemabelow a root or in a 3.0 document chooses nothing; thejsonSchemaDialectinfo; andinvalid-exclusive-bound("in this schema's dialect").docs/compilers/openapi.md#dialect-keywordsstates the rule, and#constraintsand#openapi-versionsfollow it.Left open, each named in the rule's Status and at the code drawing the line:
itemswritten as a list (refused by the load phase's table),additionalItems,dependencies,definitions,id, and the keywords beside a$ref. The table also has no rows for drafts 6, 7 and 2019-09. Both are openapi: keywords a draft before 2020-12 spells otherwise are read as 2020-12 reads them #1457, noted atoas.draft04Keywords.required: []passes (openapi: a schema read in a JSON Schema draft is held to its minor's keyword rows #1466).$idnaming its document by the 3.2$selfroots a resource of its own, so a$schemabeside it chooses a dialect, since the binder knows a document by its location alone (openapi: $self is the base URI for some references and not others #863, where references share the gap).allOfbranches writing an exclusive bound in forms that state none, beside each other or beside one that does, are called disagreeing and kept verbatim, and the bound reader's findings for them go unreported (openapi: allOf branches whose exclusive bounds state nothing read as disagreeing #1494, noted atschema.agreement.write). That holds onmainfor 3.0 and 3.1 alike; reading draft 4 lets a draft 4 schema writing numeric bounds in two branches reach it too.Closes #1449
Closes #1458
Test plan
oas(rows per dialect and minor, the fields a dialect leaves alone, every dialect row citing its text),bind(which$schemais a root: component, parameter and media type schemas; not a subschema, an example value or a property named$schema; a schema file at its root; a document's roots in pointer order, however declared),load(rows governed by node, first root wins, the source's rows in its ownjsonSchemaDialect, shared with a document naming the same, an unread dialect warned at each$schemaandjsonSchemaDialectin whichever document writes it, once for a document read under two names, never for one noDocument.Sourcesentry holds),lowering.SchemaRulesOf, ande2e.TestDocumentDialect_*(the issue's schema, 3.2, an$id-embedded resource alone and inside a root of another dialect,jsonSchemaDialectwith a root overriding it, a nested$schema, 3.0, a parameter's schema, beside a$ref, across an allOf, an allOf branch an$idembeds read in its own dialect, a schema file, an unread dialect, the OAS dialect by a published id, a malformed$schemaandjsonSchemaDialectreported by validation alone, and a$schemanaming no dialect, of any shape, choosing none in a document defaulting to draft 4). New conformance caseschema-dialects, with an embedded 2020-12 resource inside a document defaulting to draft 4; the validation oracle (canary.TestValidationOracle_AgreesWithTheLibrary) lists its two draft 4 bounds as disagreements with the parser library under a new reason,declared-dialect: the library holds every 3.1 schema to the 2020-12 meta-schema whatever dialect it names.jsonSchemaDialectreader, the rows cache keyed by dialect, the source's rows in its dialect,SchemaRulesOf, and both bound readers), each killed by the tests above. Two survived until tests were added for them: the allOf agreement reading a conjunct in the document's dialect rather than its own (the e2e casean allOf reads each branch in its own), and a reached document's rows ignoring itsjsonSchemaDialect(load.TestLoad_TheSourcesRowsAreThoseOfItsJSONSchemaDialect). A variant that reads an unlisted document's roots at no version is equivalent, since the table answers no rows for no version. The realistic variant, reading them at the source's version, is killed: it would warn at the source's root about a document the result does not hold.main(71e08b618, fix(compilers/openapi): read schema dialects from the rule table #1450 included) over 2,070 specs (real-world OpenAPI 3 documents and the repository's testdata). 2,063 are byte-identical. The 6 that differ change in diagnostic wording alone, shown by rewriting the four old messages to the new ones and comparing IR and diagnostic bytes. The 7th is the new case.openapi.json@e7ff3501, OpenAPI 3.0.3):morphic validateoutput byte-identical tomain.main:BenchmarkCompile_PetstoreatGOMAXPROCS=1, three runs each, 1,322,455 B/op and 12,122 allocs/op against 1,322,023 and 12,118. Instructions retired byvalidateatGOMAXPROCS=1 GOGC=off, runs alternating: ontestdata/golden/openapi/petstore.yaml, five each, a median of 113.21M against 113.16M; on the k8s spec, three each, 12.815G (12.805G to 12.821G) against 12.806G (12.793G to 12.811G); on the golden spec, six each, medians within 0.02%. All within noise.docs/compilers/openapi.md#dialect-keywordsalone, over a grid of 1,728 specs (version ×jsonSchemaDialect× a root's$schema× position, direct, subschema,$id-embedded in each dialect and nested below a root, × bound form), agrees with the compiler on every bound, refusal andopenapi/unread-dialectwarning;maindisagrees on 792.$schema, each named by a$reffrom a 3.1 document with external references allowed: exit codes and error sets identical tomain, no panic.openapi/unread-dialectfires at 455 roots, 409 of them draft 7's, whoseitemslists,additionalItems,dependencies,definitionsand$refsiblings read otherwise than 2020-12's.morphic-harnessoracles (round trip, determinism, idempotence, order invariance) clean on the new case and on the cross-reference, allOf,$ref-sibling, external-file, two-spelling, merge-key and alias probes.TestLoweringSpec,TestConformance, the IR census (unchanged under-update), the method cap and the doc-comment cap pass locally on the final tree.