Skip to content

Fix/error location as jsonpointer - #279

Merged
fredbi merged 8 commits into
go-openapi:masterfrom
fredbi:fix/error-location-as-jsonpointer
Aug 9, 2026
Merged

Fix/error location as jsonpointer#279
fredbi merged 8 commits into
go-openapi:masterfrom
fredbi:fix/error-location-as-jsonpointer

Conversation

@fredbi

@fredbi fredbi commented Aug 8, 2026

Copy link
Copy Markdown
Member

Change type

Please select: 🆕 New feature or enhancement|🔧 Bug fix'|📃 Documentation update

Short description

Fixes

Full description

Checklist

  • I have signed all my commits with my name and email (see DCO. This does not require a PGP-signed commit
  • I have rebased and squashed my work, so only one commit remains
  • I have added tests to cover my changes.
  • I have properly enriched go doc comments in code.
  • I have properly documented any breaking change.

fredbi and others added 5 commits August 8, 2026 15:47
Locations were assembled by string concatenation, which lost information and
produced notations no consumer could parse. They are now built from a list of
JSON pointer reference tokens and rendered on demand, so a separator can never
be confused with a token that contains one.

Locations that change:

- array items carry their index again: "a.b" becomes "a.0.b". The index was
  set on the schema validator after its sub-validators had been built, so it
  never reached the validator raising the error;
- "items[0]" and "allOf[0]" become "items.0" and "allOf.0";
- the "default" and "example" keywords are no longer appended twice;
- a response is located by its operation rather than by a bare status code:
  "200.name.default" becomes "paths./pets/{id}.get.responses.200.name.default";
- a required property missing at the root loses its leading separator:
  ".paths in body is required" becomes "paths in body is required".

The predicates telling a schema apart from plain data (isProperties, isDefault,
isExample) and the recursion guard on visited schemas walk tokens instead of
splitting on dots, so they no longer match mid-token. Trailing indices are
trimmed before the marker lookup: an element of an example is example data, and
schema-only checks stay off it.

Error names keep the dotted rendering, so messages returned by generated servers
are unchanged apart from the locations listed above. The JSON pointer rendering
is available internally; no API exposes it yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
Result now knows where every error and warning it holds came from, and reports
it as an RFC 6901 JSON pointer into the validated document:

    for _, e := range result.LocatedErrors() {
        fmt.Printf("%s: %v\n", e.Pointer, e.Err)
    }

    /definitions/Pet/name/default: ... must be of type string: "number"
    /paths/~1pets~1{id}/get/responses/200/examples/friends/0/name: ...

Errors and Warnings keep their contents and their types, so nothing downstream
has to change and no error value is wrapped. A pointer is empty when the check
that failed has no single location to name, such as a duplicate operation id or
a document that could not be read at all.

Locations are held in slices kept in step with the error slices, and carried
through merges, through the relevance filter and through the result pool. They
are recorded by the validators themselves, so a location is as precise as the
validator that reported the failure: the document-wide schema pass emits true
document pointers, while a check that only knows a parameter by name reports
the name.

Two spec messages move as a result, both now naming the operation they belong
to rather than the parameter or header alone:

- "user.items in body has invalid pattern" becomes
  "paths./foo.get.parameters.user.items in body has invalid pattern";
- "X-Foo in header has invalid pattern" becomes
  "paths./foo.get.responses.default.headers.X-Foo in header has invalid pattern".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
An invalid or dubious $ref now reports where it was declared:

    /definitions/Bad: invalid ref "file:///etc/passwd"

Both checks iterate the analyzer's references, which are handed over as values
only: its index is keyed by document location, but neither AllRefs nor
AllReferences exposes the keys. The locations are recovered instead by walking
the raw document for "$ref" members, which needs no interpretation of what a
reference points to, and keeps the index in step with the unexpanded spec the
two checks are meant to inspect.

The index is best effort. Example subtrees are skipped, since a "$ref" member
there is plain data rather than a declaration, and a reference declared more
than once keeps the smallest pointer so the answer does not depend on map
iteration order. A miss costs a misleading or empty pointer, never a wrong
verdict: nothing else is decided from it.

Diagnostics that no single location can describe still report an empty
pointer, a spread of remote hosts and a duplicate operation id among them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
A default is a value, so a "$ref" member sitting in one declares nothing and
must not be taken for the location of a reference, the same way example values
are already skipped.

The exception is "default" under "responses", which names a response rather
than holding a value, and is commonly a $ref to a shared response. That one
keeps being indexed, both at the document level and under an operation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
analysis v0.26.0 exposes AllRefsByLocation, so the reference index no longer
has to be rebuilt here by walking the raw document. Inverting the analyzer's
map replaces that walk, halving the file.

The guesswork goes with it. Deciding whether a "$ref" member was a declaration
or a value meant knowing example and default subtrees hold data, with an
exception for a default that names a response; the analyzer indexes
declarations only, so none of that has to be restated.

Locations and the two diagnostics now read the same index, which makes every
reference they can report one the index knows where to find. A $ref sitting
directly on a shared parameter or shared response is indexed by neither, so
nothing that used to be located has stopped being so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.53612% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.99%. Comparing base (af06d7e) to head (db68410).
⚠️ Report is 2 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
helpers.go 79.41% 7 Missing ⚠️
spec.go 88.13% 7 Missing ⚠️
example_validator.go 87.80% 5 Missing ⚠️
result.go 92.75% 3 Missing and 2 partials ⚠️
default_validator.go 92.10% 3 Missing ⚠️
path.go 97.64% 1 Missing and 1 partial ⚠️
ref_locations.go 86.66% 1 Missing and 1 partial ⚠️
validator.go 96.72% 2 Missing ⚠️
schema.go 95.65% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #279      +/-   ##
==========================================
+ Coverage   92.31%   92.99%   +0.67%     
==========================================
  Files          24       27       +3     
  Lines        3280     3498     +218     
==========================================
+ Hits         3028     3253     +225     
+ Misses        166      161       -5     
+ Partials       86       84       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

fredbi and others added 3 commits August 9, 2026 00:07
A required property that is never defined, and one marked both required and
readOnly, now report the entry of the required array they come from rather
than the definition holding it:

    /definitions/Pet/required/1: "notDeclared" is present in required but not
    defined as property in definition "Pet"

That is the text a reader has to go and amend, so it is what a consumer
tracking positions in the document wants to anchor on.

The search for the property descends into additionalProperties schemas, so the
two locations are carried apart: the schema being searched moves down, while
the required entry that started the search stays put.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
A parameter was reported at its name, which no document can address: they are
held in an array. Over the fixture specs, that left most pointers unresolvable
and made a consumer walk up to the array before finding anything.

Parameters are now indexed from the unexpanded document, which is the only
place the index survives: expansion merges the parameters an operation declares
with those of its path item, and resolves the ones written as a $ref. The
lookup falls back to the path item, so a parameter declared once for every
operation under a path is found from any of them.

Messages keep naming the parameter. A path token now carries a readable form
beside the addressed one, so the pointer indexes while the message reads as it
did:

    /paths/~1pets/get/parameters/1     paths./pets.get.parameters.tags

A parameter too broken to be identified, one with no name, has no index to
point at; the name stands in, as before.

One message moves, and it is a correction: a parameter declared on a path item
is no longer reported under an operation that did not declare it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
Two classes of pointer addressed nothing, so a consumer had to walk up before
finding anything to show.

A finding about something absent is now reported on the value that should hold
it: a missing required property points at the object, not at the property that
is not there. At the root that is the empty pointer, which addresses the whole
document, and Located.Pointer says so rather than calling it unknown.

Tokens a document needs to address a value, but that a message has never
spelled, are marked structural: part of the pointer, absent from the dotted
form. That puts "properties" between a schema and its members, "schema" under
a response, and the media type under its examples:

    /definitions/Pet/properties/name/default
    definitions.Pet.name.default

The predicates telling a schema apart from plain data skip structural tokens.
They ask what a value is, and plumbing must not answer: an example addressed
through its media type is still an example.

Not covered, and left as it stands: a parameter or response written as a $ref
is located where it is declared, so anything below that site resolves in the
shared definition rather than there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
@fredbi
fredbi merged commit 83d7d4e into go-openapi:master Aug 9, 2026
21 checks passed
@fredbi
fredbi deleted the fix/error-location-as-jsonpointer branch August 9, 2026 00:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant