Repository navigation
openapi: raw mapping readers disagree with the parser on merge keys and alias keys #1089
Description
Problem
The parser reads a YAML mapping through its << merge keys, and names a pair whose key is an alias by the scalar the alias stands for. nodeview.KeyOf says every reader of a mapping's raw pairs asks it, so that none can disagree with the parser about a pair's key. Fifteen production sites read raw pairs by hand instead, and they answer the two questions (which pairs, and under which key) in their own ways. Some of them lower a value the document does not write.
An example written with a merge key keeps << as a member:
openapi: 3.1.0
info: {title: t, version: "1"}
paths: {}
components:
schemas:
S:
type: object
example:
<<: {a: 1}
b: 2morphic compile exits 0 with no diagnostics, and the schema's example is:
{"kind":"object","object":[{"name":"<<","value":{"kind":"object","object":[{"name":"a","value":{"kind":"number","num":"1"}}]}},{"name":"b","value":{"kind":"number","num":"2"}}]}nodeview.View.MappingPairs, which reads a mapping as the parser does, presents the pairs b: 2 and a: 1.
An example with an alias key keeps the anchor's name as a member:
example:
&k rate: 1
other: *k
*k : 2The example's members are rate: 1, other: "rate" and k: 2, again with no diagnostic. MappingPairs presents other: rate and rate: 2: the third pair is a repeat of rate, which wins, not a member named k.
#1072's input still keeps the first copy of a key repeated through an alias ({"rate":1}), and #395's and #384's still lose keys merged in through <<.
Every site
git grep -nE '[ij] \+= 2' -- 'compilers/openapi/*.go' ':!*_test.go' lists 20 lines. Read site by site, two are nodeview's own, three are out of scope (below), and these fifteen read a mapping's pairs:
| Site | Function | How it reads keys and merges |
|---|---|---|
compilers/openapi/detect.go:433 |
settledByOwnPairs |
nodeview.KeyOf |
internal/scan/scan.go:145 |
eachChild |
nodeview.KeyOf |
internal/load/external.go:253 |
holdSpellings |
nodeview.Deref(key).Value; merged pairs not read |
internal/load/external.go:538 |
reachesLocal |
nodeview.Deref(key).Value; merged pairs not read |
internal/load/siblings.go:190, :306, :325 |
refBeside, withoutRef, writesBeside |
isRefKey, which is Deref(key).Value; merged pairs not read |
internal/load/treereads.go:104 |
index |
its own merge test, key.Value == "<<" on any scalar; Deref for names |
internal/annotation/nodes.go:43, :74, :119 |
RawChildNode, RawMappingKeys, RawChildNodes |
keyName, one alias hop through n.Alias.Value; merged pairs not read (#395, #384) |
internal/annotation/rawjson.go:280 |
mappingInto |
its own isMergeKey (value <<, tag empty, ! or !!merge) and mapKey |
internal/annotation/rawjson.go:356 |
checkUniqueKeys |
compares raw kind and value, so an alias key is no repeat (#1072) |
internal/overlay/overlay.go:456 |
children |
raw key.Value |
internal/value/value.go:192 |
mappingValue |
raw key.Value; << kept as a member (both examples above) |
That is three merge-key tests (the library's yml.IsMergeKey inside nodeview, annotation.isMergeKey, and treereads.go's own) and five key readings (KeyOf, Deref(key).Value, keyName, mapKey, and the raw kind and Value).
Cause
annotation and value cannot ask nodeview: neither allowlist in internal/archtest/arch_test.go names it, although nodeview imports only the library's yml package and yaml.v3. The other sites predate KeyOf or ask a narrower question with a reading of their own. annotation/unknown.go's unreachableKeyDiag already names the gap: resolving a merged key "needs the merge-expanded view (internal/nodeview), which no raw-node reader here has".
Direction
A draft.
- Classify each site by the question it asks: which pairs the parser presents (route through
nodeview.View.MappingPairs), which key the parser gives one pair (route throughnodeview.KeyOf), or a deliberate read of the raw tree, which keeps a comment saying why.scanruns on the source before any view exists, andoverlayaddresses the tree an overlay document names, so both may be the third kind. - Add
nodeviewto the allowlists ofannotationandvalue; it imports only the library'symlpackage,yaml.v3and the standard library, so no cycle follows. Three comments ininternal/archtest/arch_test.gothen go stale and are rewritten, not extended:annotation's,value's ("a value is decided by its own wire spelling") andnodeview's list of who reads it. - Delete
annotation.keyName,annotation.isMergeKeyandtreereads.go's merge test. The nil, document and mapping preamble thatRawChildNode,RawMappingKeysandRawChildNodeseach repeat goes with them. - openapi: a section entry whose key is no scalar is dropped without a diagnostic #947 is a defect of
KeyOfitself. Once every reader asks it, the fix lands in one place. value.mappingValueconverts one node at a time. Decide whether it takes the budgetedViewor a stateless expansion with its own bound.
Design it together with #788, which touches the same readers.
Acceptance
- The two examples above lower to the pairs
MappingPairspresents, or are reported, never silently changed. - openapi: a verbatim entry keeps the first copy of a key repeated through an alias #1072's, openapi: raw-node readers miss keys merged in through a
<<#395's and openapi: a preserved field supplied through a merge key or alias is dropped with no trace #384's reproductions lower what the document writes, or report it. - Every site in the table asks
nodeviewor carries a comment saying why it reads raw, and the command above finds no new pair read without one. - Conformance cases for a merged and an aliased key in an example, an extension and a path item.
- Goldens change only for inputs that write a merge key or an alias key.
Out of scope
- Classifying a scalar once it is reached: openapi: one classifier for YAML scalars, shared by value lowering and raw preservation #784.
- Test tooling that walks raw trees (
internal/harness/order.go,internal/openapitest/corpus.go). - Byte loops that step by two (
bind/resources.gonormalEscapes,merge/admits.gouriTailValid).
Related: #395, #384, #1072, #947, #788, #784
Priority: P4, rule 2 (corrupt-spec guard), as #1072 and #947 on the same mechanism are. A key repeated through an alias is a repeated key, and a << merge key resolves to a tag outside the JSON Schema ruleset the OpenAPI Specification's Format section limits YAML to. The golden spec is JSON, which can write neither an alias nor a merge: grep -c '"<<"' openapi.json → 0 (spec @81cb007b). The two examples above → a << member and a k member (morphic @ff8330ba).
Activity
OmarAlJarrah commentedon Oct 23, 2026
#1104 moves the
settledByOwnPairsrow's site: the function is unchanged and now sits atcompilers/openapi/internal/detect/probe.go:133, itsnodeview.KeyOfcall at line 136 (#1104's head,688af076).