Skip to content

openapi3: remove map-iteration order leaks causing flaky tests - #1158

Merged
fenollp merged 2 commits into
getkin:masterfrom
cloudnativeninja:fix/validate-extras-deterministic-order
Apr 24, 2026
Merged

openapi3: remove map-iteration order leaks causing flaky tests#1158
fenollp merged 2 commits into
getkin:masterfrom
cloudnativeninja:fix/validate-extras-deterministic-order

Conversation

@cloudnativeninja

@cloudnativeninja cloudnativeninja commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Bug

Several sites iterate a Go map with for ... range and let the
randomized iteration order leak into observable output (error strings,
returned slices). When tests assert on that output — as refs_test.go
and issue513_test.go do with require.EqualError on the exact
extra sibling fields: [...] message — the assertion flakes whenever
the map holds 2+ keys. The same pattern produces subtler
non-determinism in openapi3filter and routers/gorillamux.

These are plausible contributors to the flakes noted in the recent
"openapi3: skip v3.1 load/validation flaky tests" commit (apis-guru
validation over specs with multiple sibling extensions on $ref
nodes, and expanded paths over enum variables).

Audit methodology

Wrote a small go/ast + go/types walker that flags every
RangeStmt whose operand has *types.Map type and whose body
appends to a slice or formats a string using the iteration variable.
The walker flagged 14 candidate sites across the repo; manual review
whittled those to 4 real order-leaks — the 10 others already sort
post-append (slices.Sort), short-circuit on a bool, or write only
into another map (order-independent consumers).

Fixes (one commit each)

1. openapi3/refs.tmplvalidateExtras (generated → openapi3/refs.go)

Root cause of the test flakes on the extra sibling fields assertion.

-		for ex := range x.Extensions {
+		for _, ex := range componentNames(x.Extensions) {

componentNames is the existing helper already used one loop above
for the sibling x.extra slice — it returns a sorted []string.
Regenerated refs.go via go generate ./...; fix propagates to all
9 generated *Ref.validateExtras methods (Callback, Example,
Header, Link, Parameter, RequestBody, Response, Schema,
SecurityScheme).

2. openapi3filter/req_resp_decoder.go — form-body schema check

for propName, propSchema := range schema.Value.Properties { ...
    return nil, fmt.Errorf("unsupported schema of request body's property %q", propName)

Returned the first invalid property found. With multiple invalid
properties, which one surfaced was random. Sort names before
iteration.

3. openapi3filter/req_resp_decoder.godecodeSchemaConstructs

for name, prop := range schemaRef.Value.Properties { ...
    return fmt.Errorf("conflicting values for property %q", name)

Same pattern for the conflict-detection error.

4. routers/gorillamux/router.go — enum expansion

s := make([]string, 0, len(m))
for value := range m { s = append(s, value) }
var2val[name] = mapAndSlice{m: m, s: s}

Callers index s as mas.s[i%len(mas.s)] to build template path
parts, so different map orderings produced different sets of
expanded paths across runs (the outer parts slice is sorted, but
the set of generated parts depends on the enum permutation).
Added slices.Sort(s) after the append loop.

Behavior change

None beyond determinism. The set of properties/errors/paths produced
is identical; only the order is now stable (alphabetical).

Verification

  • go generate ./... — no spurious diff beyond the 9 intended sites in refs.go.
  • go build ./... — clean.
  • go test -count=3 ./openapi3 — 5853 tests × 3 runs, 0 failures.
  • go test -count=2 ./... — 4703 tests × 2 runs across 11 packages, 0 failures.
  • go test -run "TestRefsExtras|TestIssue513" -count=100 ./openapi3 — 1200 runs, 0 failures (was flaky before).

The AST walker used for the audit is not included in this PR but can
be shared if you'd like it vendored as a lint check.

…ests

validateExtras iterated x.Extensions via a raw map range, so Go's
randomized map iteration order leaked into the "extra sibling fields: %+v"
error string. Tests asserting on the exact message flaked whenever
Extensions held 2+ keys.

Iterate sorted keys via the existing componentNames helper instead.
Regenerated refs.go from refs.tmpl; fix propagates to all 9 Ref types
(Callback, Example, Header, Link, Parameter, RequestBody, Response,
Schema, SecurityScheme).
@fenollp fenollp changed the title fix(openapi3): sort Extensions keys in validateExtras to stop flaky tests openapi3: sort Extensions keys in validateExtras Apr 24, 2026
…on-determinism

Companion cleanup to the validateExtras fix: three more sites where
raw `for ... range map` iteration leaked order into observable output.

- openapi3filter/req_resp_decoder.go (form-body schema check): the loop
  over schema.Value.Properties returned the *first* invalid property via
  fmt.Errorf("unsupported schema of request body's property %q", ...).
  With multiple invalid properties, which one surfaced was random. Sort
  property names before iteration.

- openapi3filter/req_resp_decoder.go (decodeSchemaConstructs): same
  pattern for the "conflicting values for property %q" error. Sort
  property names before iteration.

- routers/gorillamux/router.go (enum expansion): the per-variable enum
  slice `s` was built from `for value := range m` with no sort. Callers
  index it as `mas.s[i%len(mas.s)]` to generate template path parts,
  so different map orderings produced different sets of expanded paths
  across runs. `slices.Sort(s)` after the append loop.

Findings surfaced via an AST walker that flagged every RangeStmt whose
operand had map type and whose body appended/formatted through the
iteration variable.
@cloudnativeninja cloudnativeninja changed the title openapi3: sort Extensions keys in validateExtras fix: remove map-iteration order leaks causing flaky tests Apr 24, 2026
@fenollp fenollp changed the title fix: remove map-iteration order leaks causing flaky tests openapi3: remove map-iteration order leaks causing flaky tests Apr 24, 2026
@fenollp
fenollp merged commit 5a0a337 into getkin:master Apr 24, 2026
5 checks passed
reuvenharrison added a commit to oasdiff/kin-openapi that referenced this pull request Jun 25, 2026
* Update usage message in cmd/validate (getkin#1150)

* openapi3: fix determinism when handling discriminator mappings (getkin#1151)

* feat: bump Go to 1.26 (getkin#1152)

* openapi3: use componentNames in func (*Components) Validate(..)

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: use componentNames when validating Extensions

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: replace slices.Sort() uses with componentNames()

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* ci: fix running go-generate

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: optimize generated refs Validate

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: replace slices.Sort() uses with componentNames() in generated refs

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: use maps.Copy instead of loops

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: use deterministic iteration in more (all?) places

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* refacto: use greppable type declarations

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* refacto: prefer strings.SplitSeq() when range-ing

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* feat: add OpenAPI 3.1 support

Add comprehensive OpenAPI 3.1 / JSON Schema 2020-12 support:

- Type arrays with null support (e.g., ["string", "null"])
- JSON Schema 2020-12 keywords: const, examples, prefixItems, contains,
  minContains, maxContains, patternProperties, dependentSchemas,
  propertyNames, unevaluatedItems, unevaluatedProperties
- Conditional keywords: if/then/else, dependentRequired
- ExclusiveBound union type for 3.0 (boolean) and 3.1 (numeric) exclusive bounds
- Webhooks support with $ref resolution
- Version detection helpers: IsOpenAPI3_0(), IsOpenAPI3_1()
- Info.Summary and License.Identifier fields
- JSON Schema 2020-12 validator via EnableJSONSchema2020()
- Document-level validation option: EnableJSONSchema2020Validation()
- Allow $ref alongside other keywords in 3.1 schemas
- Handle "null" type in schema validation
- Auto-detect 3.1 in cmd/validate

Co-Authored-By: Chance Kirsch <>
Co-Authored-By: RobbertDM <>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve $ref in OpenAPI 3.1 schema fields (prefixItems, contains, etc.)

The loader's resolveSchemaRef only resolved $ref in pre-3.1 fields (items,
properties, additionalProperties, not, allOf, anyOf, oneOf). References
inside the new OpenAPI 3.1 / JSON Schema 2020-12 fields were silently
left unresolved, causing nil Value pointers.

This adds ref resolution for: prefixItems, contains, patternProperties,
dependentSchemas, propertyNames, unevaluatedItems, unevaluatedProperties.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: recurse into OpenAPI 3.1 fields in transformOpenAPIToJSONSchema

The OpenAPI-to-JSON-Schema transformation only recursed into pre-3.1
fields (properties, additionalProperties, items, not, oneOf, anyOf,
allOf). Nested schemas inside 3.1 fields with OpenAPI 3.0-isms like
nullable:true were not converted, causing incorrect validation.

This adds recursion into: prefixItems, contains, patternProperties,
dependentSchemas, propertyNames, unevaluatedItems,
unevaluatedProperties.

Also consolidates the properties/patternProperties/dependentSchemas
map iteration into a single loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: validate sub-schemas in OpenAPI 3.1 schema fields

Schema.Validate() (used by doc.Validate()) recursively validates
sub-schemas in Items, Properties, AdditionalProperties, etc. but did
not recurse into the new OpenAPI 3.1 / JSON Schema 2020-12 fields.
Invalid sub-schemas nested inside these fields went undetected.

This adds validation for: prefixItems, contains, patternProperties,
dependentSchemas, propertyNames, unevaluatedItems,
unevaluatedProperties.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add const keyword validation to built-in validator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: improve OpenAPI 3.1 spec compliance

- Make paths optional in 3.1 (required only in 3.0)
- Add mutualTLS security scheme type validation
- Validate license url/identifier mutual exclusivity
- Enable JSON Schema 2020-12 validation in openapi3filter for 3.1 docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add remaining JSON Schema 2020-12 keywords and improvements

- Add $id, $anchor, $dynamicRef, $dynamicAnchor identity keywords
- Add contentMediaType, contentEncoding, contentSchema vocabulary
- Add discriminator support for anyOf (was only oneOf)
- Validate jsonSchemaDialect as valid URI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix go fmt formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve 8 correctness issues in OpenAPI 3.1 support

- IsEmpty(): add missing checks for PrefixItems, Contains, MinContains,
  MaxContains, PatternProperties, DependentSchemas, PropertyNames,
  UnevaluatedItems, UnevaluatedProperties, Examples
- JSONLookup(): add all 23 missing JSON Schema 2020-12 field cases
- validate(): relax items requirement for arrays when in 3.1 mode or
  when prefixItems is present
- transformOpenAPIToJSONSchema: clean up exclusiveMinimum/Maximum false,
  handle nullable:true without type field
- MarshalYAML: only emit paths when non-nil (valid in 3.1)
- visitConstOperation: use reflect.DeepEqual for json.Number comparison
- Webhooks validation: use componentNames() for deterministic ordering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: avoid CI lint match in OPEN_ISSUES.md

Rephrase text to not contain literal json struct tag syntax that
triggers the json/yaml tag consistency check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove OPEN_ISSUES.md from repo

Open issues are tracked in the PR getkin#1125 description instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve 8 additional OpenAPI 3.1 issues

- Add $comment keyword to Schema struct (MarshalYAML, UnmarshalJSON,
  IsEmpty, JSONLookup)
- Fix PrefixItems type from []*SchemaRef to SchemaRefs for consistency
  with OneOf/AnyOf/AllOf and JSON Pointer support
- Fix exclusiveBoundToBool data loss: preserve numeric bound value
  when converting OAS 3.1 exclusive bounds to OAS 2.0
- Auto-enable JSON Schema 2020-12 validation for OpenAPI 3.1 documents
  in doc.Validate() so library users don't need explicit opt-in
- Add ref resolution tests for if/then/else and contentSchema
- Add transform test for contentSchema with nullable nested schema
- Add validate test for contentSchema with invalid sub-schema
- Document breaking API changes in README (ExclusiveBound, PrefixItems)
- Regenerate docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add $schema, $defs keywords and fix remaining issues

- Add $schema keyword to Schema struct for per-schema dialect declaration
- Add $defs keyword (Schemas map) for local reusable schema definitions,
  with full support: struct, marshal, unmarshal, IsEmpty, JSONLookup,
  validate (recurse), loader (resolve refs), transform (recurse)
- Fix jsonSchemaDialect URI validation to require a scheme
- Refactor discriminator resolution into shared helper to eliminate
  code duplication between oneOf and anyOf paths
- Regenerate docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: strip __origin__ from Const and Examples in Schema.UnmarshalJSON

OpenAPI 3.1 adds Const (any) and Examples ([]any) fields to Schema.
Like Enum/Default/Example, these can contain arbitrary JSON/YAML values
that pick up __origin__ metadata from the YAML loader. Strip it on
unmarshal to prevent false diffs and unexpected metadata in parsed values.

Adds TestOrigin_ConstAndExamplesStripped regression test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: strip __origin__ from Encoding, Variables, and Webhooks maps

The origin-tracking YAML loader injects __origin__ as a key inside
any map-valued field to record source location. However, typed Go maps
(map[string]*Encoding, map[string]*ServerVariable, map[string]*PathItem)
treat __origin__ as a real entry, causing false positive diffs when the
same spec is loaded from two different file paths.

Fix by deleting originKey from these three maps after JSON unmarshaling,
mirroring the existing pattern used for Extensions and the unmarshalStringMapP
helper already used by Content, Schemas, Headers, etc.

Affected:
- MediaType.Encoding (map[string]*Encoding)
- Server.Variables (map[string]*ServerVariable)
- T.Webhooks (map[string]*PathItem)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: honour $ref sibling keywords in OAS 3.1 (e.g. deprecated:true)

In OpenAPI 3.0 / JSON Schema draft-07, $ref replaces its entire object
so sibling keywords are silently ignored. In OpenAPI 3.1 / JSON Schema
2020-12, $ref and sibling keywords are both applied.

Changes:
- SchemaRef gains a `sibling *Schema` field (generated via refs.tmpl)
- UnmarshalJSON populates sibling when non-extension fields appear
  alongside $ref
- resolveSchemaRef applies sibling fields onto a copy of the resolved
  schema, but only for OAS 3.1+ documents (preserves 3.0 behaviour)
- applySiblingSchemaFields overlays known annotation/metadata fields
  (deprecated, description, title, readOnly, writeOnly, example,
  externalDocs, default) using the extra[] field list so only explicitly
  present siblings are applied

Test: TestOAS31_RefSiblingKeyword in loader_31_schema_refs_test.go loads
a 3.1 spec where status has deprecated:true as a $ref sibling and asserts
that the resolved Value carries Deprecated==true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: regenerate openapi3.txt after refs.go header update

refs.go header changed from "Code generated by go generate; DO NOT EDIT."
to "Code generated by go generate using refs.tmpl; DO NOT EDIT refs.go."
Update the committed go doc snapshot to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: support boolean form for unevaluatedProperties and unevaluatedItems

JSON Schema 2020-12 allows unevaluatedProperties and unevaluatedItems
to be either a boolean or a schema object (e.g. `unevaluatedProperties: false`).
The fields were typed as *SchemaRef, which caused an unmarshal error
when encountering the boolean form.

Introduce BoolSchema, a type that handles both boolean and schema forms
(same pattern as AdditionalProperties), and use it for all three fields.
AdditionalProperties becomes a type alias for BoolSchema so existing
code continues to compile unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: regenerate openapi3.txt after BoolSchema type change

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump yaml3 to v0.0.12 for unconditional field origin tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: verify origin tracking for mapping-valued schema fields

Adds TestOrigin_MappingFields covering dependentRequired,
dependentSchemas, and patternProperties — fields that were missing
from Origin.Fields before yaml3 v0.0.12.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update README changelog entry to v0.136.0

Rename the v0.132.0 entry to v0.136.0 (the upcoming release for this
PR), move it above v0.135.0 for chronological order, and add the
UnevaluatedItems/UnevaluatedProperties → BoolSchema type change.

Addresses fenollp's review on getkin#1125.

* openapi3: set OpenAPI version-picking API to doc.IsOpenAPI30, doc.IsOpenAPI31OrLater and doc.OpenAPIMajorMinor

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: make sure API is tested from outside its package

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: replace EnableJSONSchema2020Validation func with IsOpenAPI31OrLater

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: simplify some code

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: tune openapi3.1 new fields doc comments

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: lets not skip checking errors

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: fix impl of (Schema) MarshalYAML() (any, error)

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: rename visitJSONWithJSONSchema to useJSONSchema2020

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: fix (License) MarshalYAML()

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: fix license field validation, post 3.1

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: fix openapi struct validation, post 3.1

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: fix validating schema.Default

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* fix and optimize generated ref Validate

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: fix: schema sibling fields are an OpenAPI3.1 feature

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: notes on TestSchemaRefSiblingKeyword

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: finally document func (*Schema) VisitJSON(any, ...SchemaValidationOption) error

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: revert regression on validating SecurityScheme.Type==mutualTLS for v3.0

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: complete applySiblingSchemaFields with all Schema fields

Pierre's FIXME on PR #15: the helper previously covered only 8 fields
(deprecated, description, title, readOnly, writeOnly, example,
externalDocs, default). Extend to every field in the Schema struct so
any sibling keyword alongside $ref in an OAS 3.1 document is correctly
overlaid onto the resolved $ref target.

Section comments dropped: the entire function runs only when the caller
has already checked IsOpenAPI31OrLater(), so splitting cases by OAS
version inside the switch was misleading.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* openapi3: extract validateExtras helper for all Ref types

Split the $ref sibling-fields check out of (*Ref).Validate into a
dedicated validateExtras method, generated from refs.tmpl for every
Ref type. This lets subsequent patches invoke validateExtras on
SchemaRefs referenced from inside a schema tree — i.e. at the sites
where Schema.validate recurses into nested SchemaRefs — without
duplicating the check logic.

Template update + `go generate` regeneration. No behaviour change:
Validate still returns the same error for the same inputs; the check
is just reachable from additional call sites.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* openapi3: validate $ref siblings at nested SchemaRefs, fixing 3.0 regression

TestSchemaRefSiblingKeyword/{3.0 false false} (added by @fenollp in
this PR) exposed that $ref sibling fields (e.g. description,
deprecated) were accepted on nested property schemas in OAS 3.0 when
they should have been rejected. Root cause: SchemaRef.validateExtras
was invoked only from SchemaRef.Validate, but nested schema
validation in Schema.validate recursed directly into
ref.Value.Validate, bypassing the ref-level extras check.

Invoke ref.validateExtras(ctx) at every nested SchemaRef site: items,
properties, additionalProperties, prefixItems, contains,
patternProperties, dependentSchemas, $defs, propertyNames,
unevaluatedItems, unevaluatedProperties, contentSchema, if/then/else.

Drop the t.Skip on TestSchemaRefSiblingKeyword now that all three
subcases pass.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* openapi3: update TestIssue601 — lxkns.yaml has description siblings to $ref

lxkns.yaml is OAS 3.0.2 and uses `description` as a sibling to $ref in
many places, which is invalid in OAS 3.0 ($ref replaces the enclosing
object; siblings are ignored). Previously this was silently accepted
because validateExtras was only invoked on top-level component refs.
Now that nested SchemaRefs are validated too, those siblings surface
as a validation error and mask the example-type mismatch this test
actually targets.

Pass AllowExtraSiblingFields("description") so validation proceeds to
the example-type assertion the test was written for (same pattern
already used in issue513_test.go:264).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* openapi3: gate OAS 3.1 schema fields from OAS 3.0 validation

Per Pierre's PR #15 review: Schema.validate accepted OAS 3.1 / JSON
Schema 2020-12 fields (prefixItems, contains, if/then/else, $defs,
$schema, patternProperties, unevaluatedItems, etc.) on OAS 3.0
documents. Add a gate at the top of Schema.validate that rejects
each 3.1-only field in 3.0 mode via errFieldFor31Plus.

The gate honours AllowExtraSiblingFields so 3.0 documents that resolve
external JSON Schema refs (draft-04, draft-07) can opt in to letting
$id/$schema/etc. through. This matches the existing opt-in mechanism
already used by TestExtraSiblingsInRemoteRef.

This whole block becomes a no-op once OAS 3.0 gets its own standalone
schema validator.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* openapi3: update tests for OAS 3.1 field gate

Three tests need adjustment now that OAS 3.1-only schema fields are
rejected under OAS 3.0 validation:

- TestSchemaIfThenElse_Validate and TestSchemaValidate31SubSchemas
  exercise 3.1 features (If/Then/Else, PrefixItems) directly via
  Schema.Validate without a doc, so the isOpenAPI31OrLater flag is
  never set from a doc version. Pass IsOpenAPI31OrLater() explicitly.

- TestIssue495WithDraft04{,Bis} load OAS 3.0 documents that $ref
  external JSON Schema draft-04 meta-schemas (which contain $id and
  $schema). Add AllowExtraSiblingFields("$id", "$schema") so the
  tests assertion about the unresolved inner "#" ref surfaces as
  intended, matching the pattern already used in issue513_test.go.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* fix: handle git ref paths in join() for $ref resolution

When resolving relative $ref paths against a base path that uses git ref
syntax (e.g. "origin/main:openapi.yaml"), path.Dir() treats the colon as
a regular character and returns "origin" instead of recognizing it as the
git ref separator.

This causes $ref resolution to produce invalid paths like
"origin/schemas/pet.yaml" instead of "origin/main:schemas/pet.yaml",
breaking multi-file OpenAPI specs loaded via git refs.

The fix detects the colon separator and applies path.Dir/path.Join only
to the file path portion after the colon, preserving the git ref prefix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* openapi3: add JoinFunc callback for custom path resolution

Replace the colon-specific handling with a general-purpose JoinFunc
callback on Loader. This lets callers override how relative $ref paths
are resolved against the base path — useful when loading specs from
non-filesystem sources (git objects, remote archives, etc.) where the
base path follows a different convention than filesystem paths.

When JoinFunc is nil (the default), behavior is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: regenerate openapi3.txt after JoinFunc addition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: lowercase example output to pass CI lint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: remove redundant TestJoinFunc (covered by ExampleLoader_JoinFunc)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove trailing blank line in loader_test.go

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* openapi3: add many many tests with TestV3ApisGuruOpenapiDirectory

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: record failures as openapi3/testdata/apis_guru_openapi_directory golden

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi2: add many many tests with TestV2ApisGuruOpenapiDirectory

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi2: record failures as openapi2/testdata/apis_guru_openapi_directory golden

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: record failures after rebasing on top of getkin#1125

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: enable testing for 3.1 documents

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: record v3.1 load/validation test failures

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: skip v3.1 load/validation flaky tests

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: remove map-iteration order leaks causing flaky tests (getkin#1158)

* openapi2conv: nil-guard components lookup in FromV3SchemaRef (getkin#1156)

* fix(openapi2conv): nil-guard components lookup in FromV3SchemaRef

FromV3SchemaRef indexes components.Schemas[name] whenever the input
schema has a . Several recursive call sites in this file
(FromV3RequestBodyFormData among them) already pass components=nil
when descending into array items / nested refs, so hitting a ref
from that path nil-dereferenced and took down the whole V3→V2
conversion. In practice this fires on real-world inputs like the
OpenAI OpenAPI 3.0 spec, where CreateEmbeddingRequest references a
sub-schema inside an array of formData items (getkin#1062).

Guard the components lookup with a components != nil check. When
the components table is not available we fall through to the same
'return the plain ' branch that already handles non-binary
schema refs - semantically 'the target schema is not known locally,
emit a pass-through ref' - instead of panicking. Runtime behaviour
for callers that do pass a non-nil Components is unchanged.

Closes getkin#1062

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* test(openapi2conv): regression test for getkin#1062 (per @fenollp)

Adds TestIssue1062_FormDataArrayOfRefDoesNotPanic with a minified
spec capturing the OpenAI OpenAPI 3 shape that triggered the original
report: a multipart/form-data request body whose property is an array
whose items are a $ref into #/components/schemas/.

Without the fix in this PR, the test panics inside FromV3 (line 723)
because FromV3RequestBodyFormData passes components=nil into
FromV3SchemaRef, which then nil-derefs on components.Schemas[name].
With the fix, the test passes and the resulting v2 spec contains the
expected formData parameter named 'documents'.

Signed-off-by: SAY-5 <say.apm35@gmail.com>

---------

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Signed-off-by: SAY-5 <say.apm35@gmail.com>
Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>

* address various lint errors

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* refacto: replace `openapi3.*Ptr(..)` funcs with `new(..)`

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* refacto(tests): use t.Context instead of context.Background

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* fix and upgrade goimports-reviser

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* revert to go 1.25 and revert cc4f8d9

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3gen: clear nullable on exported component bodies

When a struct is reached via *T, generateSchemaRefFor sets
schema.Nullable=true to encode nullability of that one reference site.
With ExportComponentSchemas on, that same body becomes the canonical
component definition shared by every $ref site, so the nullable flag
leaks into the component itself. Codegen tools (Orval, openapi-typescript)
then emit broken types like `interface Foo {...} | null`.

Clear the flag right before registering the schema as a component.

* openapi3: add test for issue getkin#927 (nullable not respected on $ref schemas)

* test: move public-API tests to external _test packages (getkin#1168)

Co-authored-by: Tommy Nacass <tommy.nacass@gmail.com>

* feat(openapi3): add per-type validation errors with cluster wrappers (getkin#1166)

* feat(openapi3conv): canonicalization pass for 3.0 -> 3.x (getkin#1162)

* openapi3conv: test Upgrade on many documents (getkin#1169)

* feat(openapi3): batch-convert long-tail RequiredFieldError sites (getkin#1170)

* feat(openapi3): MutuallyExclusiveFieldsError cluster for forbidden field pairs

Converts four mutual-exclusion sites:
- example.value vs externalValue
- mediaType.example vs examples
- license.url vs identifier
- link.operationId vs operationRef

* feat(openapi3): add SchemaReadOnlyWriteOnlyExclusive leaf to MutuallyExclusiveFieldsError

* feat(openapi3): ForbiddenFieldError cluster for set-but-not-allowed fields

Converts four 'field MUST NOT be set in this context' sites:
- header.name (given by the headers map key)
- header.in (implicitly 'header')
- OAuth flow authorizationUrl (wrong flow type)
- OAuth flow tokenUrl (wrong flow type)

* feat(openapi3): RequiredFieldError leaves for parameter.name, responses non-empty, apiKey securityScheme.name

* feat(openapi3): ServerURLTemplateError cluster for server URL template failures

Converts three server URL template sites:
- mismatched { and }
- undeclared variables (template/Variables count mismatch)
- undeclared variables (declared name not in URL template)

* feat(openapi3): EitherFieldRequiredError cluster and SchemaItemsRequired leaf

- example.value or externalValue
- link.operationId or operationRef
- schema.items required when type=array (RequiredFieldError leaf)

* feat(openapi3): RequiredFieldError leaves for doc-root info/paths and jsonSchemaDialect URI

Three new RequiredFieldError leaves at the doc-validation root:
- info (must be an object)
- paths (must be an object — 3.0 only)
- jsonSchemaDialect (must be an absolute URI with a scheme)

* feat(openapi3): SchemaBothFormsExclusive cluster for union-typed schema fields set both ways

Three sites in schema.go where a BoolSchema-typed field has both forms set:
- additionalProperties
- unevaluatedItems
- unevaluatedProperties

* feat(openapi3): ExactlyOneFieldError + SingleEntryContentError clusters for parameter/header content+schema

Four sites:
- parameter.go content/schema must be exactly one
- parameter.go content map must contain at most one entry
- header.go content/schema must be exactly one
- header.go content map must contain at most one entry

* test: use require.ErrorContains per repo lint

* feat(openapi3): WebhookNilError for nil pathitem in T.Validate webhook walk

Single-site cluster carrying the offending webhook key name.

* docs: regenerate openapi3.txt for combined branch

* openapi3gen: skip component export for anonymous types

Anonymous nested struct types have an empty reflect.Type.Name(), so
generateTypeName returns "". Registering them under that key produces
"#/components/schemas/" refs that violate the OpenAPI spec (component
keys must match ^[a-zA-Z0-9._-]+$) and break codegen tools like Orval.
Inline the schema instead, matching the existing behavior for generics
and time.Time.

* feat: migrate to oasdiff/yaml v0.1.0 single Unmarshal API + enable DisableTimestamps

Bumps:
  * github.com/oasdiff/yaml         v0.0.9  -> v0.1.0
  * github.com/oasdiff/yaml3        v0.0.12 -> v0.0.13

yaml v0.1.0 consolidates three unmarshal entry points (Unmarshal,
UnmarshalWithOriginTree, UnmarshalWithDecodeOpts) into a single
Unmarshal with the signature:

    Unmarshal(y []byte, o interface{}, decode DecodeOpts, opts ...JSONOpt) (*OriginTree, error)

DecodeOpts carries Origin (tracking) and DisableTimestamps (YAML 1.1
implicit-timestamp resolution opt-out, backed by yaml3 v0.0.13).

This PR migrates every yaml.Unmarshal / yaml.UnmarshalWithOriginTree
call site (4 production sites + 8 test sites) to the new signature,
setting DisableTimestamps: true everywhere. Real-world OpenAPI specs
use date-shaped strings as map keys (e.g. revision dates like
"1344-08-22") which the YAML 1.1 default resolves to time.Time —
that resolution is the root cause of validation errors that surface
as misleading time.Time-stringified values in error messages, and
in some cases breaks string-keyed lookup entirely.

Testdata golden files in openapi3/testdata/apis_guru_openapi_directory/
regenerated by the existing golden helper:
  * Several previously-failing specs now validate cleanly (golden
    files removed by the helper's os.Remove path).
  * Others show updated error messages where date-shaped scalars no
    longer appear as stringified time.Time.

`go test ./...` clean across all packages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: fixup lint after modifications to marsh.go

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: re-enable tests disabled due to YAML dates in map keys

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* un-patch YAML serialization of dates (see issue getkin#697)

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: typed context errors for Validate() wrapper chain (getkin#1183)

* feat(openapi3): typed context errors for Validate() wrapper chain

Replaces the 14 fmt.Errorf wrap sites in Validate() with three typed
error types carrying their context as structured fields:

- SectionContextError{Section, Cause}    — wraps an error inside one of
  the top-level document sections (info, paths, components, security,
  servers, tags, externalDocs, webhooks, jsonSchemaDialect)
- PathContextError{Path, Cause}          — wraps an error inside a
  specific path
- OperationContextError{Method, Cause}   — wraps an error inside a
  specific HTTP-method operation

Continues the typed-validation-error work from getkin#1166 and getkin#1180:
those PRs typed the leaf errors; this one types the wrapper layers
that carry doc-tree position around them.

Why: today, callers that want to render context separately (which
section a finding lives in, which path, which operation) have to
parse the rendered error string with regex. errors.As against typed
wrappers is the structured equivalent.

Backward compatibility: Error() strings are byte-identical to the
fmt.Errorf wrappers they replace. Existing consumers parsing the
rendered text continue to work unchanged. The typed extraction is
purely additive.

Wrap sites converted:
- openapi3.go: 9 sections (info, paths, components, security,
  servers, tags, externalDocs, webhooks, jsonSchemaDialect)
- paths.go: per-path wrapper (PathContextError)
- path_item.go: per-operation wrapper (OperationContextError)
- operation.go, tag.go, schema.go: 3 additional externalDocs sites
  (also use SectionContextError{Section: "external docs"})

Tests cover Error() format byte-stability, Unwrap chain walking,
errors.As extraction from a three-layer chain (section + path +
operation), and arbitrary non-typed inner causes. All existing
tests pass unchanged across openapi2, openapi3, openapi3conv,
openapi3filter, openapi3gen, and the routers — confirming the
rendered strings are stable.

.github/docs/openapi3.txt regenerated via docs.sh.

* refactor(openapi3): rename context errors to *ValidationError

Addresses review feedback on getkin#1183:

- SectionContextError    -> SectionValidationError
- PathContextError       -> PathValidationError
- OperationContextError  -> OperationValidationError

The new names tie these positional wrappers to the ValidationError
family they belong to (base ValidationError, cluster types, leaves).

Also renames the file to sit in that namespace:
section_context_error.go -> validation_error_context.go (+ _test.go).

No behavior change: Error() strings and Unwrap() chains are untouched.
.github/docs/openapi3.txt regenerated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* openapi3: track Origin on the document root (T) (getkin#1184)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* openapi3: tests flakiness corrected (getkin#1159)

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* openapi3: aggregate independent validation errors via EnableMultiError (getkin#1185)

* openapi3: aggregate independent validation errors via EnableMultiError

Today Validate returns the first error it encounters and stops, so a
spec with several independent defects only reports one at a time: fix,
re-run, see the next, repeat. With the new EnableMultiError
ValidationOption, container validators aggregate independent problems
and return them together as a flat MultiError.

Aggregation happens at the container fan-out points where it is safe
to continue past an error: T, Paths, PathItem, Operation, Components,
Responses. Leaf validators (Schema, Info, Server, etc.) still fail
fast, since continuing past a leaf error can hit a nil deref.

The result shape is flat: each top-level MultiError entry is one
fully-wrapped chain (SectionValidationError -> PathValidationError ->
OperationValidationError -> typed leaf). Consumers iterating the
MultiError get one entry per problem; consumers using errors.As /
errors.Is work unchanged thanks to MultiError.As / MultiError.Is.

By default Validate returns the first error encountered, byte-identical
to today: the existing test suite passes through the same code path it
always did.

* openapi3: keep "return validateExtensions" pattern in container validators

CI greps for "return .*validateExtensions" in openapi3/ and asserts the
count equals the number of Extensions fields. The previous commit moved
those calls to "me.emit(validateExtensions(...))" + "return me.result()",
breaking the count.

Add a finalize helper on errCollector that emits its argument and returns
the accumulated result, so each container's trailing line is a single
"return me.finalize(validateExtensions(ctx, X.Extensions))" that both
reads cleanly and matches the CI fence regex.

* openapi3: address review feedback on EnableMultiError

Top-level:
 - cmd/validate: add --multi flag (mirrors --defaults / --examples / --patterns)
 - Convert simple leaves to aggregate independent errors: License, Info,
   Server (singular + plural), Tag (singular + plural), ExternalDocs.
   Each is sequential independent checks with no guard-then-deref pattern,
   structurally identical to the existing container validators. Schema,
   Parameter, MediaType have internal cross-field dependencies and are left
   for follow-up PRs after per-method analysis.
 - Rewrite EnableMultiError docstring: drop the explicit fan-out list (would
   go stale), name the transition explicitly, explain why some leaves are
   not yet converted, and replace the generic errors.As / errors.Is note
   with a concrete how-to.

Inline:
 - validate_multi_error_test.go: use require.ErrorContains for chain checks;
   tighten GreaterOrEqual to Equal where the count is exact.
 - response.go: route Responses.Validate's empty-responses error through
   me.emit for consistency with the rest of the multi-error path.
 - components.go, paths.go, openapi3.go: add comments at the error-path
   continue statements explaining why we skip descending past a malformed
   parent key (component name, path key, nil webhook).
 - error_collector.go (new): move the errCollector type and methods out of
   validation_options.go so the options file stays focused on options.
 - openapi3.go: revert to the wrap-variable reassignment pattern per
   section block; keep wrapSection as the factory.

* test: hoist err.Error() out of require.Equal to satisfy CI grep fence

* openapi3: wrap plural examples Validate errors in newSchemaValueError

Parameter.Validate and MediaType.Validate already use newSchemaValueError
for the singular `example:` field, but the plural `examples:` map loop
returned a bare fmt.Errorf wrapping the underlying *SchemaError. That
meant errors.As(err, &SchemaValueError) didn't match for plural examples,
so downstream consumers fell back to a generic catch-all instead of
recognising the example-violates-schema cluster.

Wrap the plural-loop result in newSchemaValueError("example", ...) too,
preserving the example key inside the wrap so the rendered message still
names the offending example. Aligns the plural path with the singular
path; both now surface as *SchemaValueError to errors.As consumers.

Existing example_validation_test.go assertions and the apis_guru golden
fixtures gain the "invalid example: " prefix the wrap adds.

* openapi3: point Examples Validate failures at the example's value, not the parent

For plural Examples entries on Parameter/MediaType, the Validate failure
used the parent struct's Origin, which deep-links to the parameter or
media-type start (e.g. the `in: query` line) rather than the offending
example value. Build the SchemaValueError's Origin from the Example
struct's per-field origin (`Origin.Fields["value"]`) when available,
falling back to the example's struct origin, then to the parent.

No message-format change: existing string assertions continue to hold,
only the resolved line/column tightens to the example's `value:` field.

* openapi3: keep validating extensions when responses is empty

The empty-responses path used 'return me.result()' which short-circuited
before validateExtensions could run, so extension errors would never
aggregate with the empty-responses finding under multi mode. Drop the
early return and let control fall through to the trailing finalize() call;
the for loop is a no-op on an empty Responses, so the only behaviour
change is that validateExtensions now runs.

Spotted by @fenollp in PR review.

* test: regression pin for the empty-responses + extension aggregation fix

Adds TestResponses_Validate_EmptyAndExtensionAggregate which constructs
an empty Responses with a non-x- sibling on Extensions and asserts both
findings surface under EnableMultiError. Reverting the previous commit's
fall-through edit causes this test to fail (one leaf instead of two).

* openapi3: fix validation of duplicated path templates (getkin#1189)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* openapi3: type the remaining bare-error validation sites (getkin#1187)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [WIP] Origin: carry block end (EndLine/EndColumn) on Key

Location gains EndLine/EndColumn; originFromSeq parses the trailing
end_delta/end_col yaml3 now emits and sets them on Origin.Key, so each
operation's Origin spans the whole endpoint block. Existing Key golden
assertions updated.

NOTE: go.mod is on a dev-local replace => ../yaml3 (not committed).
Repin to the released yaml3 (v0.0.14, once #9/#10 merge) before pushing.

* Bump oasdiff/yaml3 to v0.0.14 + oasdiff/yaml to v0.1.1; record Origin block end

Picks up Node end positions and the __origin__ block-end span, so Origin.Key now
carries EndLine/EndColumn (the span of the block a location heads). Updates the
TestOrigin_Example expectation to the corrected flow-collection end (the value
{"bar": "baz"} now ends just past its closing brace).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Regenerate openapi3.txt API docs for Origin EndLine/EndColumn

---------

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Signed-off-by: SAY-5 <say.apm35@gmail.com>
Co-authored-by: Pierre Fenoll <pierrefenoll@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: cloudnativeninja <tommy.nacass@gmail.com>
Co-authored-by: Sai Asish Y <say.apm35@gmail.com>
Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>
Co-authored-by: 0-don <don.cryptus@gmail.com>
Co-authored-by: Brandon Bloom <brandon@brandonbloom.name>
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.

2 participants