Skip to content

openapi: raw mapping readers disagree with the parser on merge keys and alias keys #1089

Closed

Description

@OmarAlJarrah

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: 2

morphic 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 : 2

The 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.

  1. 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 through nodeview.KeyOf), or a deliberate read of the raw tree, which keeps a comment saying why. scan runs on the source before any view exists, and overlay addresses the tree an overlay document names, so both may be the third kind.
  2. Add nodeview to the allowlists of annotation and value; it imports only the library's yml package, yaml.v3 and the standard library, so no cycle follows. Three comments in internal/archtest/arch_test.go then go stale and are rewritten, not extended: annotation's, value's ("a value is decided by its own wire spelling") and nodeview's list of who reads it.
  3. Delete annotation.keyName, annotation.isMergeKey and treereads.go's merge test. The nil, document and mapping preamble that RawChildNode, RawMappingKeys and RawChildNodes each repeat goes with them.
  4. openapi: a section entry whose key is no scalar is dropped without a diagnostic #947 is a defect of KeyOf itself. Once every reader asks it, the fix lands in one place.
  5. value.mappingValue converts one node at a time. Decide whether it takes the budgeted View or a stateless expansion with its own bound.

Design it together with #788, which touches the same readers.

Acceptance

Out of scope

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

  1. added
    type:bugDefect in shipped behavior
    priority:P3Later emitters, M4 and up, 3.2-only, streaming, webhooks, rare constructs
    on Oct 22, 2026
  2. added
    priority:P4Refactors, tests, CI, hostile-input hardening, cosmetic diagnostics, other formats
    and removed
    priority:P3Later emitters, M4 and up, 3.2-only, streaming, webhooks, rare constructs
    on Oct 22, 2026
  3. OmarAlJarrah commented on Oct 23, 2026

    @OmarAlJarrah
    MemberAuthor

    #1104 moves the settledByOwnPairs row's site: the function is unchanged and now sits at compilers/openapi/internal/detect/probe.go:133, its nodeview.KeyOf call at line 136 (#1104's head, 688af076).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:P4Refactors, tests, CI, hostile-input hardening, cosmetic diagnostics, other formatstype:bugDefect in shipped behavior

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions