Skip to content

fix(compilers/openapi): warn on a 3.0 array without items - #724

Merged
OmarAlJarrah merged 11 commits into
mainfrom
fix/3.0-array-items-required
Oct 14, 2026
Merged

OmarAlJarrah merged 11 commits into
mainfrom
fix/3.0-array-items-required

Conversation

@fuad-daoud

@fuad-daoud fuad-daoud commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

OpenAPI 3.0 requires items on a schema whose type is array. JSON Schema 2020-12, which 3.1 and 3.2 use, makes it optional. A 3.0 array schema that writes no items lowered without a word: the document declares an array and never says what its elements are, and nothing tells the caller. This change reports it once per declaration, as a warning, at the schema's own pointer. Every type the IR holds is byte-identical to before, and the document only gains the warning. The element type was already the top type, so nothing the document declared is dropped.

Rule. In a 3.0 document, a schema whose type is or includes array and which writes no items reports one openapi/missing-array-items warning. 3.1 and 3.2 are silent, and so is a 3.0 schema that wrote items, including items: {}. The check keys on the keyword's absence, never on what the element lowered to.

Where it runs: the declaration, not one lowering of it. The check sits in schemaBody. Every non-reference schema body passes through it, above the dispatch that picks the lowering. So the rule reaches every array declaration whatever it lowers to:

  • a List;
  • a Tuple, when the schema writes prefixItems;
  • a union of literals or an empty Enum, for a non-scalar or empty enum;
  • a literal, for const;
  • the node an allOf or oneOf beside it builds;
  • a Union, for a type list holding array, reported at the schema rather than at a variant.

Two alternatives were tried and rejected:

  • The List branch of lowerArray. This PR's earlier revision put the check there. It missed every array that lowers to anything else. A sweep of 1,613 real-world 3.0 specs found one such array, a non-scalar enum with type: array, which drew no warning there.
  • A walk over the source document in load, the way the duplicate-parameter rule is checked. A walk would also reach subschemas the compiler keeps verbatim, but it splits this rule from the other 3.0 schema rule, the boolean exclusiveMinimum, which the lowering checks. It also needs a second site for schemas in other documents, which the source walk never reaches. The duplicate-parameter rule needed that second site too, in the lowering.

Boundaries, each deliberate.

  • A $ref site is silent. It never reaches schemaBody, and 3.0 ignores whatever is written beside a $ref, so {$ref: …, type: array} declares no array.
  • A schema kept verbatim is never read as a schema, so it is not held to the rule. That covers one under not, an inline allOf branch, a union kept beside a body, and the schema of a component parameter or header that nothing references. The 3.0 exclusive-bound check has the same boundary: on main, not: {type: integer, exclusiveMinimum: 5} reports nothing, and neither does that bound in an unreferenced component parameter or header.
  • The dialect is the entry document's, as it already is for exclusive bounds. A 3.1 file reached by $ref from a 3.0 root is read as 3.0, and the reverse; that is openapi: a schema in another document is read in the root's dialect #839. A schema in another document is reported at each source $ref that reaches it rather than once, as main already reports an exclusive bound there.
  • A 3.0 prefixItems still lowers to a tuple, which is openapi: a 3.0 document's prefixItems lowers to a tuple with no diagnostic #820. It now draws this warning about items, which is accurate. openapi: a 3.0 document's prefixItems lowers to a tuple with no diagnostic #820's own defect, a 2020-12 keyword read in 3.0 without a word, is untouched.

Severity: warning. Nothing declared is dropped, and this is a document defect that still lowers. That is the same shape ReservedHeaderName, UnknownObjectKey and EmptyEnum are warnings for in diag.go. An error would also stop harness.Check at the first one and hide every later finding in the spec. It is a code of its own rather than DegradedConstruct, because a code is what CI allowlists a finding by (docs/architecture.md §4) and nothing is kept in a weaker shape.

Pointer: the schema's own. The missing keyword has no node in the source, so no /items position exists to name. Sibling missing-construct findings locate at the schema (EmptyEnum).

The dialect question. lowering.Ctx.Is30 replaces ExclusiveBoundIsBoolean, and both dialect readings ask it. It is named after Ctx.Since32, the other dialect predicate on Ctx. An unrecognized version, and a zero-value context, answer false. nullable is still read in every dialect without asking, and whether it should be is #632.

Lowered twice, reported once. A declaration that a $ref to one of its coordinates reaches first is lowered again when the declaration itself is lowered (Types.InternDeclared), and the check reports both times. The caller sees one warning, because compile.Diags drops identical diagnostics.

Closes #650.

Test plan

  • TestIs30_FollowsTheDeclaredVersion (3.0.0 and 3.0.3 true; 3.1.0, 3.2.0, 4.0.0 and "" false) and TestIs30_TheZeroContextReadsAs2020.
  • TestLower_ListWithoutItems: a bare array lowers to a List over t/prim/any in 3.0.3, 3.1.0 and 3.2.0 alike.
  • TestLower_MissingArrayItems counts what the lowering returns, before compile.Diags deduplicates.
    • 3.0.3 rows that report once at the expected pointer: a bare array, a nullable one, a property, an array as another's items, a type list ([array, string], at the schema), a tuple, an enum of arrays, an empty enum, a const, an allOf beside it, a oneOf beside it, and a component a sibling's $ref names.
    • Silent rows: items: {}, array keywords without a type ({minItems: 1}), a $ref site's sibling type: array, an array under not, and 3.1.0 and 3.2.0.
  • TestLower_MissingArrayItemsLoweredTwiceWarnsOnce covers the coordinate-$ref path in both orders. It asserts the raw report count (2 ref-first, 1 declaration-first), so the test is known to reach the double lowering, and one warning after compile.Diags.
  • TestCompile_MissingArrayItemsReachesTheDiagnostics compiles the issue's exact document end to end. It asserts that exactly one warning reaches Compile's diagnostics, at the schema's pointer and as a warning, and that the document still holds a List over the top type.
  • Conformance case missing-array-items (3.0.3, with the coordinate $ref declared before its target) asserts the full set of reports the document draws: a component, an enum of arrays, a property and a query parameter. Two arrays that write items stay silent. It is the only golden added or changed.
  • diag_test.go's codes() lists the new code.
  • Each mutation was planted, confirmed to build, run against the tests above, and restored. A no-op change left every test green.
    • Dropping the call from schemaBody reddens every reporting row, the double-lowering test and the conformance case.
    • Restoring the check to lowerArray's List branch only reddens the tuple, enum of arrays, empty enum, const, allOf and type-list rows, and the conformance case.
    • Dropping the dialect conjunct reddens the 3.1 and 3.2 rows, and two 3.1 conformance goldens.
    • Dropping the type conjunct, or the items conjunct, reddens the silent rows it guards.
    • Reporting twice reddens the counting rows, and reporting as info reddens every reporting row.
    • Moving the pointer reddens every reporting row.
    • Widening Is30 to 3.2 reddens its table and the 3.2 row.
    • Accepting only a single array type reddens the type-list row.
    • Disabling compile.Diags deduplication for this code reddens the ref-first double-lowering row and the conformance case.
  • Sweep at 3309b1e over the 1,613 OpenAPI 3.0 specs and 227 OpenAPI 3.1/3.2 specs in a local collection of third-party projects' test fixtures, compiled with main's binary and this branch's:
    • The 3.0 specs drew 11 warnings across 4 files. That equals the number of type: array mappings without items in their sources, and each warning's pointer was checked against its source.
    • The 3.1 and 3.2 specs drew none, beside 14 such legal arrays.
    • No other diagnostic and no exit code differs from main.
  • make gate (fmt, vet, lint, build, coverage, fuzz, bench-smoke) green at 3309b1e, with coverage a 100% statement gate.

An OpenAPI 3.0 array schema that writes no `items` now reports
`openapi/missing-array-items` at the schema's own pointer, and its element
type stays the top type exactly as before. The 2020-12 dialects (3.1 and 3.2)
are silent, and so is a 3.0 schema that wrote the keyword — including
`items: {}`. The guard keys on the keyword's absence, never on the element
having lowered to `any`.

A 3.0 document that writes `prefixItems` reaches the tuple lowering, where
`items` is never consulted; what that reveals is a separate defect, left
untouched here.
@fuad-daoud fuad-daoud self-assigned this Sep 30, 2026
@fuad-daoud fuad-daoud changed the title fix(compilers/openapi): warn on a 3.0 array without items (#650) fix(compilers/openapi): warn on a 3.0 array without items Sep 30, 2026

@OmarAlJarrah OmarAlJarrah left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review of the merged result (this branch with main merged in), probed with 3.0 specs rather than read.

Must fix: the two doc comments over the 100-word cap (inline). go test ./internal/archtest is the only failing package on the merged tree; the rest of go test ./... and golangci-lint on the touched packages pass.

Inline, in order of weight: the unfiled prefixItems defect, the "at most once per pointer" claim that does not hold on main, the once-ness tests that cannot see a duplicate, the entry-document dialect limit, Ctx method budget and duplicated tests, expensive-first condition order, and a garbled test comment.

Not attached to a diff line:

  1. No corpus spec reaches the new code. I validated all 241 files under testdata/ with a build from main and one from this branch merged with main: identical output for every file, no missing-array-items anywhere. Only two documents there are OpenAPI 3.0 (streaming-media-30, nullable-30) and neither contains an array. The testing section of the repo's contributor guide says the usual way to cover a new construct is a spec the order-invariance sweep reaches; the siblings empty-enum, path-item-anchored-key and querystring-forbidden-keywords each have a conformance case with a golden. A small testdata/conformance/openapi 3.0 case with the $ref declared before the array's owner would put the new code under the sweep and the golden diff.
  2. Description claim with no test. The description lists "a variant pointer under type: [...]" as a possible location. It works ({type: [array, string]} in 3.0.3 reports at /components/schemas/T/type/0), but no row pins it. Add a row or drop the mention.
  3. Description wording. The "compile.Types.Intern runs the build closure at most once per pointer" sentence needs the same correction as the code comment (see the inline note on accumulate.go), and the "separate defect, left as it is here" sentence needs the issue number once filed.

Checked and fine: every carrier I tried warns once at its own pointer (path, query, header and cookie parameters; request and response bodies including default; headers; form and multipart properties; callbacks; shared components referenced from several operations, in both path orders); items: {}, items: true, nullable, $ref siblings and the 3.1/3.2 dialects stay silent; the oracle sweep passes on every valid probe. Unreferenced components/parameters, headers, requestBodies and responses are never lowered at all, for any diagnostic, so their silence here is existing reach, not part of this change.

Comment thread compilers/openapi/internal/diag/diag.go Outdated
Comment on lines +378 to +398
// MissingArrayItems reports an OpenAPI 3.0 array schema that writes no
// `items`. 3.0 states the array form through the keyword alone, so such a
// schema declares an array without saying what its elements are — a
// document defect in that dialect (GitHub #650). The schema still lowers,
// as a List whose element type is the top type, exactly as it would
// without a diagnostic.
//
// Warning, for the reason its neighbours ReservedHeaderName and
// UnknownObjectKey are: it is a document defect that still lowers, and
// harness.Check stops at the first error diagnostic, which would hide
// every later finding in any spec containing such an array. Not an error
// for the second reason too — nothing the document declared is dropped,
// since the element type the IR carries is the one it carries today, so
// there is no losslessness failure to escalate (invariant 2 is not in
// question).
//
// A code of its own rather than DegradedConstruct, because codes are the
// unit a consumer allowlists (ir-design §13). DegradedConstruct names a
// construct kept in a weaker shape; here the document wrote no shape to
// weaken, and the lowering is lossless — it is the *document* that is
// incomplete, which is the class EmptyEnum and UnknownObjectKey name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doc comment is 192 words; the cap is 100 (TestDocComments_StayUnderTheCap, added in #745 after this branch's base). The gate was green when this was pushed, but with main merged in go test ./internal/archtest fails:

diag.go:334: MissingArrayItems: doc comment is 192 words; the cap is 100

The severity reasoning is already in the PR description, which is where the excess belongs. A version that fits (checked against the archtest on the merged tree):

Suggested change
// MissingArrayItems reports an OpenAPI 3.0 array schema that writes no
// `items`. 3.0 states the array form through the keyword alone, so such a
// schema declares an array without saying what its elements are — a
// document defect in that dialect (GitHub #650). The schema still lowers,
// as a List whose element type is the top type, exactly as it would
// without a diagnostic.
//
// Warning, for the reason its neighbours ReservedHeaderName and
// UnknownObjectKey are: it is a document defect that still lowers, and
// harness.Check stops at the first error diagnostic, which would hide
// every later finding in any spec containing such an array. Not an error
// for the second reason too — nothing the document declared is dropped,
// since the element type the IR carries is the one it carries today, so
// there is no losslessness failure to escalate (invariant 2 is not in
// question).
//
// A code of its own rather than DegradedConstruct, because codes are the
// unit a consumer allowlists (ir-design §13). DegradedConstruct names a
// construct kept in a weaker shape; here the document wrote no shape to
// weaken, and the lowering is lossless — it is the *document* that is
// incomplete, which is the class EmptyEnum and UnknownObjectKey name.
// MissingArrayItems reports an OpenAPI 3.0 array schema that writes no
// `items`, so declares an array without saying what its elements are. It
// still lowers, as a List over the top type. A warning rather than an
// error: nothing the document declared is dropped, and harness.Check stops
// at the first error diagnostic. A code of its own rather than
// DegradedConstruct, because nothing is kept in a weaker shape.

It also drops the (ir-design §13) cite. §13 defines Diagnostic and says codes are stable, but the allowlist text is in docs/architecture.md §4 and engine/diag.go. The same cite already exists at line 22, so this is inherited rather than new; it just gets copied here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied your text in 491c471, with the §13 cite dropped. go test ./internal/archtest is green on the merged tree.

Comment on lines +290 to +300
// ArrayItemsRequired reports whether this document's dialect requires an array
// schema to write `items`: true under OpenAPI 3.0, where the keyword is how the
// array form is stated at all, and false under the 2020-12 dialect of 3.1 and
// 3.2, where omitting it is legal and leaves an array with no constraint on its
// elements. An unrecognized version defaults to false, the 2020-12 answer, for
// the reason ExclusiveBoundIsBoolean beside it defaults to the numeric form.
//
// It reads the document version, which makes it a question about the context
// rather than about any schema, and it is read through its accessor so a
// context with no document answers like an unrecognized version rather than
// panicking — which is how the other readers here behave on a zero value.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also over the cap: 120 words against 100. With main merged in the archtest reports lowering.go:355: Ctx.ArrayItemsRequired: doc comment is 120 words; the cap is 100. The last paragraph repeats what ExclusiveBoundIsBoolean's comment already says about the zero value. A version that fits, which also states the entry-document behaviour discussed on the next line:

Suggested change
// ArrayItemsRequired reports whether this document's dialect requires an array
// schema to write `items`: true under OpenAPI 3.0, where the keyword is how the
// array form is stated at all, and false under the 2020-12 dialect of 3.1 and
// 3.2, where omitting it is legal and leaves an array with no constraint on its
// elements. An unrecognized version defaults to false, the 2020-12 answer, for
// the reason ExclusiveBoundIsBoolean beside it defaults to the numeric form.
//
// It reads the document version, which makes it a question about the context
// rather than about any schema, and it is read through its accessor so a
// context with no document answers like an unrecognized version rather than
// panicking — which is how the other readers here behave on a zero value.
// ArrayItemsRequired reports whether this document's dialect requires an array
// schema to write `items`: true under OpenAPI 3.0, false under the 2020-12
// dialect of 3.1 and 3.2. An unrecognized version, and a context with no
// document, answer false, as ExclusiveBoundIsBoolean does. It reads the entry
// document's version, whichever document holds the schema.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Applied in 491c471. The comment is now on Ctx.IsOpenAPI30, since the two accessors were merged into one (see the thread below), and it states the entry-document limit.

// context with no document answers like an unrecognized version rather than
// panicking — which is how the other readers here behave on a zero value.
func (c Ctx) ArrayItemsRequired() bool {
minor, _ := load.SupportedMinor(c.Doc.GetOpenAPI())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The dialect is read from the entry document (c.Doc), not from the document that holds the schema, although Ctx tracks foreign scope (foreign/holder). With allow-external-refs=true, a 3.0.3 root whose response is a $ref into an openapi: 3.1.0 file with an inline schema: {type: array} gets missing-array-items ("3.0 requires items") for a schema that is legal in its own document. The reverse (3.1 root, 3.0 file) stays silent.

This is the same limit ExclusiveBoundIsBoolean has, and there it is worse: a 3.1 file's numeric exclusiveMinimum reached from a 3.0 root is reported as an invalid-exclusive-bound error. So I would not block on it, but a limitation like this should be stated where the next reader will reach it, and neither the comment nor the PR description says it. The suggested doc comment above covers the code side; the description could carry one sentence.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed it shouldn't block. The limit is now stated in IsOpenAPI30's doc comment (491c471): it reads the entry document's version, so a 3.1 file reached by $ref from a 3.0 root is read as 3.0. The PR description will carry the same sentence, along with the note that ExclusiveBoundIsBoolean already had this limit.

// rather than about any schema, and it is read through its accessor so a
// context with no document answers like an unrecognized version rather than
// panicking — which is how the other readers here behave on a zero value.
func (c Ctx) ArrayItemsRequired() bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Design note: Ctx goes from 17 to 18 methods (the cap is 20), and this accessor is the same two lines as ExclusiveBoundIsBoolean: a version re-parse through load.SupportedMinor and a == "3.0" comparison. The description names a next dialect defect (a 3.1-only keyword read from a 3.0 document), which would take another method and another copy of the explanation.

One isOpenAPI30 helper, or a single dialect accessor, that both call would keep this at one method per question about the dialect rather than one per 3.0-ism, and lets one test table serve both.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Took the single-accessor option: ExclusiveBoundIsBoolean and ArrayItemsRequired are now one Ctx.IsOpenAPI30(), and both call sites pass its answer (491c471). Ctx is back to 17 methods, and the next 3.0-ism costs no new method.

diags = append(diags, tupleDiags...)
return t
}
if c.ArrayItemsRequired() && s.GetItems() == nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: the conjunction runs the expensive operand first. ArrayItemsRequired() calls load.SupportedMinor, which does a strings.SplitN and a concatenation, for every array schema lowered (every 3.1/3.2 array, and every 3.0 array that does write items), before the nil check on GetItems() that rules most of them out. s.GetItems() == nil && c.ArrayItemsRequired() is the same condition. I swapped them locally and the package tests stay green, as they should.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Swapped to s.GetItems() == nil && c.IsOpenAPI30() (491c471).

Comment on lines +127 to +130
// prefixItems does not reach this branch at all — the Tuple
// lowering above never consults items — and what that reveals, a
// 3.1-only keyword read from a 3.0 document with no diagnostic, is
// a separate defect deliberately left untouched here (GitHub #650).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The 3.1-only-keyword-in-a-3.0-document defect is described here, in the commit message and in the description as "a separate defect, left untouched", but there is no issue for it. I searched the tracker (prefixItems, "3.1-only keyword", "dialect", open 3.0 titles): #597 is about tuple openness, not this. (GitHub #650) points at the issue this PR closes, so after merge the next reader has nowhere to follow it.

A 3.0.3 document with type: array and prefixItems: [{type: string}] compiles with no diagnostic at all today. Please file it and cite that number here and in the description (the repo convention is to record an out-of-scope defect somewhere the next reader will reach).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed: a 3.0.3 document with type: array and prefixItems: [{type: string}] compiles to a Tuple with no diagnostic at all. I searched the tracker too and found nothing covering it (#597 and #146 are different defects). An issue is being opened, and its number will be cited in this comment and in the description.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Filed as #820, and cited in the guard comment (ac85578) and in the PR description.

Comment on lines +132 to +133
// internNode runs this build at most once per pointer, so a sibling
// $ref that reaches the declaration first still reports it once.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This claim is not true on main. internNode goes through Types.InternDeclared, which, when a reference built the node first, "builds it again under the same ID" (its own doc comment, and #750: it replaces what the reference built, not what it reported). So the guard fires twice for the same pointer when the reference comes first.

Reproduction: 3.0.3 with A.properties.copy: {$ref: '#/components/schemas/Holder/properties/tags'} declared before Holder.properties.tags: {type: array}. With a print in the guard it fires twice for /components/schemas/Holder/properties/tags in that order and once with the two swapped. The user sees one warning in both because compile.Diags drops identical diagnostics, so the observable behaviour is right, but the reason is the dedup, not once-ness of the build. The description makes the same claim ("compile.Types.Intern runs the build closure at most once per pointer"); both should say the single report comes from Diags deduplication.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reproduced. With a counter on the raw lowering diagnostics, the coordinate-$ref-first order reports twice at /components/schemas/Holder/properties/tags, and the declaration-first order reports once. The comment now says the declaration is rebuilt by Types.InternDeclared, and that the caller sees one warning because compile.Diags drops the identical second report (491c471). The description will be corrected the same way.

wantWarnings: []string{"/components/schemas/Holder/properties/tags"},
},
{
name: "3.0 sibling $ref to the bare array warns once",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These "warns once" rows cannot detect a duplicate report. lowerSpec collects through compile.Diags, which drops identical diagnostics, so a guard that appended the warning twice leaves TestLower_ListWithoutItems and TestCompile_MissingArrayItemsReachesTheDiagnostics green (I tried exactly that). Moving the pointer to /items, changing the severity, dropping either conjunct all turn tests red, so only the once-ness claim is unguarded.

This row also has a single declaration order and a plain-name $ref, which never takes the rebuild path (see the comment on accumulate.go). A row where the $ref targets the coordinate #/components/schemas/Holder/properties/tags and is declared before Holder exercises it, and counting the raw lowering diagnostics (before Diags) is what would make "once" an assertion rather than a name.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 51148c3. TestLower_ListWithoutItems now counts what LowerComponentSchemas returns, before Diags, so a guard that appends twice reddens it. I planted exactly that and it went red, in that test only. Every row in the table builds each node once. The rebuild path has its own test, TestLower_ListWithoutItemsRebuiltWarnsOnce. It runs the #/components/schemas/Holder/properties/tags $ref in both orders and asserts the one warning the caller sees. Disabling Diags deduplication reddens its ref-first subtest only.

Comment on lines +198 to +201
// TestArrayItemsRequired_TheZeroContextReadsAsOptional completes the set the
// readers above start: every one answers on a context with nothing in it, and
// this one reaches through the document pointer like its neighbour, so it is
// the other one that would panic instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment is a copy of the neighbour's with the referents mangled: "the other one that would panic instead" has no referent (this accessor reaches through the document pointer exactly as ExclusiveBoundIsBoolean does), and "completes the set the readers above start" was already said about that test. Something that says what the test pins:

Suggested change
// TestArrayItemsRequired_TheZeroContextReadsAsOptional completes the set the
// readers above start: every one answers on a context with nothing in it, and
// this one reaches through the document pointer like its neighbour, so it is
// the other one that would panic instead.
// TestArrayItemsRequired_TheZeroContextReadsAsOptional pins that a context
// with no document answers false rather than panicking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Rewritten in 491c471. The test is now TestIsOpenAPI30_TheZeroContextReadsAs2020, and its comment says what it pins: no document answers false rather than panicking.

// alone, while 2020-12 (3.1 and 3.2) leaves it optional; a lowering that
// reported the wrong way would either warn on every legal 3.1 array or stay
// silent on the 3.0 defect this accessor exists to report.
func TestArrayItemsRequired_FollowsTheDialect(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test, its table (the same six versions, in the same order) and the zero-context test above are clones of the ExclusiveBoundIsBoolean pair. If both accessors share one helper (see the note on lowering.go), one table can cover the dialect question and each accessor needs only its own one-line test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

With the single accessor, one table (TestIsOpenAPI30_FollowsTheDialect) and one zero-context test cover the dialect question, and the clones are gone (491c471).

ExclusiveBoundIsBoolean and ArrayItemsRequired were the same two lines,
a version re-parse and a "3.0" comparison, each with its own copy of the
explanation and its own test table. Every later 3.0-ism would have added
another method to Ctx, which sits near the 20-method cap. Both become
Ctx.IsOpenAPI30, one accessor tested by one table, and Ctx is back to the
method count it had before the missing-items warning.

Its doc comment now states the limit both readers share: the dialect is
the entry document's, so a 3.1 file reached by $ref from a 3.0 root is
read as 3.0.

The missing-items guard tests the nil items first, so the version is
parsed only for the arrays that could warn. Its comment no longer claims
the build runs once per pointer: a declaration a $ref reached first is
built again by Types.InternDeclared, and the single warning the caller
sees comes from compile.Diags dropping the identical second report.

MissingArrayItems's doc comment is cut to fit the 100-word cap; the
severity reasoning lives in the change that introduced it.
TestLower_ListWithoutItems collected its warnings through compile.Diags,
which drops identical diagnostics, so a guard that reported twice left
every "warns once" row green. The rows now count what the lowering
returns, and a doubled append reddens them. A row pins the variant
pointer under `type: [array, string]`.

The one path that does report twice, a $ref naming a property's
coordinate declared before its owner, gets its own test over both
orders, asserting the single warning the caller sees.

The corpus had no 3.0 document with an array, so the order-invariance
sweep never reached the warning. missing-array-items.yaml declares the
coordinate $ref first and reports at a component, a property and a
query parameter, beside two arrays that write items and stay silent.
@fuad-daoud

Copy link
Copy Markdown
Collaborator Author

On the notes not attached to a line:

  1. Corpus reach. Added testdata/conformance/openapi/missing-array-items.yaml with its golden (51148c3). It is 3.0.3, and Copy's $ref to #/components/schemas/Holder/properties/tags is declared before Holder, so the order-invariance sweep and the golden diff now reach the rebuild path. The spec warns once each at a component, a property and a query parameter, and items: {type: string} and items: {} stay silent. Disabling the guard reddens TestConformance/missing-array-items. No other golden changed.
  2. Variant pointer. Pinned: a {type: [array, string]} row expects the warning at /components/schemas/T/type/0 (51148c3).
  3. Description wording. The once-per-pointer sentence will be corrected to credit Diags deduplication, and the issue number for the prefixItems defect will be added once it is filed. The description will also pick up the IsOpenAPI30 change, the entry-document limit, and the new conformance case.

The comment on the missing-items guard names the 3.0 prefixItems case as a separate defect; it is now tracked as #820.
IsOpenAPI30's doc comment said every 3.0-ism asks it, but `nullable` is
read in every dialect without asking, and whether it should be is #632.
The comment now says which readings ask and names that exception, and
cites #839 for the entry-document limit it already stated.

TestIsOpenAPI30_FollowsTheDialect's comment drops the same claim, and
says the reverse misreading warns on the 3.1 arrays that omit items,
not on every 3.1 array.

The rebuild test's comment tied the double report to a property's
coordinate. A $ref to an `items` or to a parameter's schema, lowered
before the declaration holding it, runs the guard twice the same way,
so the comment names the mechanism rather than the one instance the
test builds.
…equired

params.go takes main's side: main moved the parameter's constraint read
into schema.ConstraintsOf, which now asks IsOpenAPI30 where the branch
asked it inline. Since32's comment named ExclusiveBoundIsBoolean, which
this branch renamed, and the missing-array-items golden is regenerated
for main's IR version and parameter IDs.
Main added Ctx.Since32 beside this branch's IsOpenAPI30, so the context
now asks two dialect questions in two grammars. IsOpenAPI30 becomes Is30,
the form its neighbour set, and its tests take Since32's test names.
The check sat in lowerArray's List branch, so it saw only the arrays that
lower to a List. A 3.0 schema writing type array and no items that lowers
to anything else was silent: one with prefixItems (a Tuple), a non-scalar
or empty enum, a const, an allOf or oneOf beside it. A type list holding
array warned at its variant pointer rather than at the schema. All of them
break the same 3.0 rule.

The check now runs in schemaBody, the step every non-reference schema body
passes through above the dispatch that picks its lowering, so it keys on
the declaration rather than on the node built for it. A $ref site never
reaches it, which is right for 3.0, where what is written beside a $ref is
ignored. A subschema kept verbatim, as under not, is never read as a
schema and is not held to it, the same boundary the 3.0 exclusive-bound
check has.

The message no longer names an element type, which a tuple, an enum or a
literal does not have. The conformance case gains an enum of arrays and
now asserts every report the document draws, not only the expected ones.
…equired

Main's IR version moved to 0.12.0, so the missing-array-items golden
this branch adds is regenerated; nothing else in it changes.
TestCompile_MissingArrayItemsReachesTheDiagnostics filtered, counted and
checked the severity and pointer of the warning by hand, which is what
missingItemsAt already does for the lowering's own tests, and repeated a
non-nil check parseFull makes. It now compares the pointer list the
helper returns, which also fails on a second report.
@OmarAlJarrah
OmarAlJarrah merged commit 2c65775 into main Oct 14, 2026
1 check passed
@OmarAlJarrah
OmarAlJarrah deleted the fix/3.0-array-items-required branch October 14, 2026 13:20
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: an OpenAPI 3.0 array schema without items is accepted silently

2 participants