Repository navigation
Description
Summary
ir.Parameter.Unmodeled and the ir.Property.Unmodeled a header lowers to each
carry entries from two different source objects, both unscoped:
- the object's own — its
x-*(fix(compilers/openapi): read x-* at every object that carries it #345), itsallowEmptyValue, itsxmlhints,
and its undeclared keys (fix(compilers/openapi): keep keys the source model does not name #356); - everything its schema had no home for — the keywords an elected lowering
could not read (fix(compilers/openapi): preserve keywords no lowering reads #348), the validation-only keywordsannotation.Readkeeps,
and the schema's undeclared keywords (fix(compilers/openapi): keep keys the source model does not name #356).
A parameter and its schema are two objects at two pointers, so both can spell the
same key. They then spell one entry, and which one survives is decided by
lowering order rather than by the document.
Reproduction
Verified at 6fcfd8a. Two shapes, both losing the parameter's own key:
# the schema's undeclared keyword takes the entry (#356 on both sides)
- name: p
in: query
divisibleBy: FROM_PARAMETER_OBJECT
schema: {type: string, divisibleBy: FROM_SCHEMA}
# the keyword #348 keeps beside a $ref takes it
- name: p
in: query
format: FROM_PARAMETER_OBJECT
schema: {$ref: '#/components/schemas/S', format: FROM_SCHEMA_BESIDE_REF}Each compiles to a single openapi:divisibleBy / openapi:format on the
parameter, holding the schema's value. The same happens on a response header,
whose object keys and schema keywords share the ir.Property it lowers to.
Current state
#356 no longer drops the losing key in silence — it reports
openapi/unknown-key-entry-taken, naming the pointer of the construct holding
the entry. The key is still represented in the IR in no form at all, so this is a
report of the loss, not a fix for it.
The fix, and why it is not #356's
Applying the rule #345 already established for the objects that ride on a carrier
they do not own — key them by the source path from the carrier down — means the
schema's entries become openapi:schema/<keyword> on a parameter and a header,
while the object's own stay unscoped.
That is a one-line change per site and a wide change in effect: it moves keys
#348 and the validation-only reader already publish, so every golden carrying
an unhomed keyword or an if/not on a parameter or header is rewritten. It is
also a decision about the meaning of a carrier's namespace, which is worth
settling once for all three mechanisms rather than as a side effect of the census
that happened to surface it.
Properties are deliberately not in scope: a property is its schema, so its
carrier has only one namespace and its entries must stay unscoped.
Activity
fix(compilers/openapi): report the entries the census cannot claim
OmarAlJarrah commentedon Sep 30, 2026
One more question this issue will have to answer: which value wins when a parameter and its schema carry the same key. Today the parameter's does, because its extensions are merged after the schema's (
MergeUnmodeledis a plain overwrite):keeps
FROM_PARAMonly, with no diagnostic. Keys that describe the value, such as a sensitivity flag or a field mapping, are more often written on the schema, so a precedence rule (or keeping both under separate namespaces, which is what this issue proposes) should say which one a consumer sees.docs: name what a promotion inherits from #1029 and #396
docs: state each limit of a promotion where it lives
OmarAlJarrah commentedon Nov 1, 2026
#1229 makes this reach a typed field, in the opposite direction to the two shapes above. For an
x-*key on both objects,annotation.MergeUnmodeledoverwrites the schema's entry with the object's, in silence, and a promotion reads the entry that is left:- {name: a, in: query, x-sensitive: false, schema: {type: string, x-sensitive: true}}acompiles with nosecretand oneopenapi:x-sensitiveholdingfalse, with nothing reported, thoughfalsestates nothing elsewhere and clears no redaction. A header does the same, throughapplyHeaderAnnotations. The schema'struereaches the IR in no form, which it does onmaintoo; what changes is that a typed field now rests on it.docs/compilers/openapi.md#extension-promotionnames this in its Status.Off the golden spec: a walk of
openapi.jsonover its 12,124 parameters and headers finds none writing a mapped key on both the object and its schema (spec @319dc756).docs(compilers/openapi): state the summary a carrier loses
OmarAlJarrah commentedon Nov 2, 2026
One more carrier of two objects in one namespace once #1333 lands, which follows a content entry's
$refintocomponents/mediaTypes. At a content-style parameter or header, thesummarythe fold takes from the use site or an alias is kept asopenapi:content/<media type>/summary, and asummarythe media type's declaration writes, which a Media Type Object does not define, is the census's under the same key.The parameter and the header each hold only
The use., andopenapi/unknown-key-entry-takenreports the declaration'ssummaryat/components/mediaTypes/M/summaryas represented in no form, once for each. The body keeps both:Content.Docs.SummaryisThe use., andContent.Unmodeled["openapi:summary"]holdsThe declaration.morphic @1afc54ee (the head of #1333).