Repository navigation
Description
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/schemasthe 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
OmarAlJarrah commentedon Oct 20, 2026
#1016 settles this for schema
$refs. At 83ab508 an unresolved schema$refgives the compiler's own reason (bind.Hop.Why), and both reproducers above now end init 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, nocomponents.schemasand no such key, read alike for schemas.OmarAlJarrah commentedon Oct 21, 2026
Since #1016, the two
$refroutes word the same miss differently: a schema$refgives the compiler's reason, and a reference object gives the library's. That now holds for misses inside another document too. Verified at17ad2ea1(main, with--opt allow-external-refs=true).feat/engine-owned-ioat89a27c5eprints the same lines; once it merges, the cross-document lines come out in default mode too, since that branch reads relative$refs by default.ok.yamldeclares onlyPandB;null.yamlholdsnull.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 thenull.yamlparameter also draws is filed separately as #1044.OmarAlJarrah commentedon Nov 2, 2026
A case where the resolver's tail is not only worded in its own vocabulary but names the wrong key. When a
$refmisses 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:The first line holds on
main@7f849127. The second needs #1333, which follows content entries intocomponents/mediaTypes; since the parser models no such section, every miss there reads this way. A mapping for this shape would say the entryNopeis not declared under/components/mediaTypes.