Skip to content

openapi: an unresolved-ref message ends in the resolver library's internal wording #809

Open

Description

@OmarAlJarrah

Problem

An openapi/unresolved-ref diagnostic from the resolve phase ends with the resolver library's raw error text, and that text is the library's vocabulary rather than the spec author's. A single missing component produces two different tails depending on whether the document declares components.schemas at all:

error openapi/unresolved-ref a.yaml#/components/schemas/Pet/properties/owner: unresolved $ref "#/components/schemas/Owner": not found -- key Owner not found in sequencedmap.Map
error openapi/unresolved-ref b.yaml#/paths/~1things/get/responses/200/content/application~1json/schema: unresolved $ref "#/components/schemas/Missing": not found -- struct is nil at /components/schemas

The location and the quoted $ref are right, and precise. The trailing clause is what does not read as a message about the spec: sequencedmap.Map is a Go type, and struct is nil at /components/schemas means "this document has no components.schemas", which a reader has to guess.

Reproduction

Verified at e0c9762 (main).

# a.yaml: components.schemas exists, the component does not
openapi: 3.1.0
info: {title: t, version: "1"}
components:
  schemas:
    Pet:
      type: object
      properties:
        owner: {$ref: "#/components/schemas/Owner"}
paths: {}
# b.yaml: the document declares no components at all
openapi: 3.1.0
info: {title: t, version: "1"}
paths:
  /things:
    get:
      operationId: listThings
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Missing"}

morphic validate a.yaml and morphic validate b.yaml print the two lines above and exit 1.

Cause

failureDiag (compilers/openapi/internal/load/references.go) builds the message as "unresolved $ref %q: %s" with err.Error(), which its comment calls the resolver's reason. That error comes from github.com/speakeasy-api/openapi (v1.25.5), whose text is written for its own callers.

The same tail is visible in #385's output for the path-item, parameter and response positions, where the issue was about the missing site and not about the wording.

Expected

The reason is stated once, in the spec's terms. For the two cases above, for example:

  • no schema named "Owner" in #/components/schemas
  • the document declares no #/components/schemas

Where the resolver's text is not a shape morphic recognizes, keeping it is still better than dropping it, so the mapping can cover the common cases and fall back to the raw text.

This is the wording counterpart of #769, which asks for a parser-worded message to be dropped where a clearer diagnostic already covers the site. It does not change the code, the site or the severity of the diagnostic, and it is not a correctness problem.

Activity

  1. added
    priority:P4Refactors, tests, CI, hostile-input hardening, cosmetic diagnostics, other formats
    on Oct 19, 2026
  2. OmarAlJarrah commented on Oct 20, 2026

    @OmarAlJarrah
    MemberAuthor

    #1016 settles this for schema $refs. At 83ab508 an unresolved schema $ref gives the compiler's own reason (bind.Hop.Why), and both reproducers above now end in it names nothing in the document it points into. A reference object's message still ends in the library's wording, byte for byte as before. The two cases the Direction proposes telling apart, no components.schemas and no such key, read alike for schemas.

  3. OmarAlJarrah commented on Oct 21, 2026

    @OmarAlJarrah
    MemberAuthor

    Since #1016, the two $ref routes word the same miss differently: a schema $ref gives the compiler's reason, and a reference object gives the library's. That now holds for misses inside another document too. Verified at 17ad2ea1 (main, with --opt allow-external-refs=true). feat/engine-owned-io at 89a27c5e prints the same lines; once it merges, the cross-document lines come out in default mode too, since that branch reads relative $refs by default.

    openapi: 3.1.0
    info: {title: t, version: '1'}
    paths:
      /x:
        get:
          parameters:
            - $ref: '#/components/parameters/Nope'
            - $ref: 'ok.yaml#/Nope'
            - $ref: 'null.yaml'
          responses:
            '204': {description: ok}
    components:
      schemas:
        A:
          $ref: '#/components/schemas/Nope'
        C:
          $ref: 'ok.yaml#/Nope'
        D:
          $ref: 'null.yaml'

    ok.yaml declares only P and B; null.yaml holds null.

    param  #/components/parameters/Nope → not found -- struct is nil at /components/parameters/Nope
    schema #/components/schemas/Nope    → it names nothing in the document it points into
    param  ok.yaml#/Nope                → not found -- key Nope not found in yaml mapping at /Nope
    schema ok.yaml#/Nope                → it names nothing in the document it points into
    param  null.yaml                    → unable to resolve reference: null.yaml
    schema null.yaml                    → it names a position that holds no schema
    

    The one miss reads two ways at present. Giving the reference-object route the schema route's reasons (bind.Hop.Why), rather than writing a third mapping, would make them read alike. The extra validation finding the null.yaml parameter also draws is filed separately as #1044.

  4. OmarAlJarrah commented on Nov 2, 2026

    @OmarAlJarrah
    MemberAuthor

    A case where the resolver's tail is not only worded in its own vocabulary but names the wrong key. When a $ref misses inside a section the parser's model does not hold, so the resolver reads it as raw YAML, the clause names the section as the key not found, though the section is there and only the entry is missing:

    openapi: 3.2.0
    info: {title: T, version: "1"}
    x-params:
      P: {name: p, in: query, schema: {type: string}}
    paths:
      /a:
        get:
          operationId: a
          parameters: [{$ref: '#/x-params/Nope'}]
          responses:
            "200":
              description: ok
              content:
                application/json: {$ref: '#/components/mediaTypes/Nope'}
    components:
      mediaTypes:
        M: {schema: {type: string}}
    error openapi/unresolved-ref a.yaml#/paths/~1a/get/parameters/0: unresolved $ref "#/x-params/Nope": not found -- key x-params not found in core model or YAML node at /x-params
    error openapi/unresolved-ref a.yaml#/paths/~1a/get/responses/200/content/application~1json: unresolved $ref "#/components/mediaTypes/Nope": not found -- key mediaTypes not found in core model or YAML node at /components/mediaTypes
    

    The first line holds on main @7f849127. The second needs #1333, which follows content entries into components/mediaTypes; since the parser models no such section, every miss there reads this way. A mapping for this shape would say the entry Nope is not declared under /components/mediaTypes.

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:featureNew capability or enhancement

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions