Summary
OpenAPI says a parameter (and a header, which follows the parameter rules) MUST contain either a
schema property or a content property, but not both. A document that writes both is invalid,
and the compiler accepts it in silence — taking one and discarding the other, with no diagnostic
in either channel.
Worse, the two positions disagree about which one survives:
fillParamType (compilers/openapi/internal/operation/params.go:97) tries content first and
returns, so schema is dropped.
headerSchema (compilers/openapi/internal/operation/content.go:455) tries schema first and
returns, so content is dropped.
Reproduction
openapi: 3.1.0
info: {title: t, version: '1'}
paths:
/x:
get:
operationId: getX
parameters:
- name: p
in: query
schema: {type: integer}
content:
application/json: {schema: {type: string}}
responses:
"200":
description: ok
headers:
X-H:
schema: {type: integer}
content:
application/json: {schema: {type: string}}
Compiled at dbf0054 with -skip-validate: .diagnostics is null, parameter p lowers to
t/prim/string (the content entry won) and header X-H lowers to t/prim/integer (the schema
won). Neither discarded declaration is preserved anywhere in the document.
Why it is wrong
Two things, separately.
The document is self-contradictory and nothing says so. A user who wrote both keywords by accident
gets a type they did not choose, and no way to find out which of the two the compiler read.
And the same source shape lowers differently at two positions the specification governs by one
rule, so which keyword wins depends on where the object sits rather than on anything the document
said.
The compiler already has the right pattern for competing keyword families that cannot all lower:
schema.dispatchOf/recordSkippedFamilies elect a winner among const/enum/allOf in a fixed
order and then keep every loser verbatim under Unmodeled with a diagnostic naming it, so the
election is visible in the output rather than only in the code. singleContentEntry on the same
path does the same for a content map with more than one entry (GitHub #139).
Expected
One order applied at both positions, the losing keyword kept verbatim under Unmodeled, and a
diagnostic naming which keyword was passed over — matching how the compiler already handles every
other case of "the source declared more than this position can lower".
Summary
OpenAPI says a parameter (and a header, which follows the parameter rules) MUST contain either a
schemaproperty or acontentproperty, but not both. A document that writes both is invalid,and the compiler accepts it in silence — taking one and discarding the other, with no diagnostic
in either channel.
Worse, the two positions disagree about which one survives:
fillParamType(compilers/openapi/internal/operation/params.go:97) triescontentfirst andreturns, so
schemais dropped.headerSchema(compilers/openapi/internal/operation/content.go:455) triesschemafirst andreturns, so
contentis dropped.Reproduction
Compiled at
dbf0054with-skip-validate:.diagnosticsisnull, parameterplowers tot/prim/string(the content entry won) and headerX-Hlowers tot/prim/integer(the schemawon). Neither discarded declaration is preserved anywhere in the document.
Why it is wrong
Two things, separately.
The document is self-contradictory and nothing says so. A user who wrote both keywords by accident
gets a type they did not choose, and no way to find out which of the two the compiler read.
And the same source shape lowers differently at two positions the specification governs by one
rule, so which keyword wins depends on where the object sits rather than on anything the document
said.
The compiler already has the right pattern for competing keyword families that cannot all lower:
schema.dispatchOf/recordSkippedFamilieselect a winner amongconst/enum/allOfin a fixedorder and then keep every loser verbatim under
Unmodeledwith a diagnostic naming it, so theelection is visible in the output rather than only in the code.
singleContentEntryon the samepath does the same for a content map with more than one entry (GitHub #139).
Expected
One order applied at both positions, the losing keyword kept verbatim under
Unmodeled, and adiagnostic naming which keyword was passed over — matching how the compiler already handles every
other case of "the source declared more than this position can lower".