Skip to content

fix(validator,converter): validate every schema, not only two positions - #426

Merged
erraggy merged 2 commits into
mainfrom
fix/schema-traversal-reaches-all-schemas
Aug 1, 2026
Merged

fix(validator,converter): validate every schema, not only two positions#426
erraggy merged 2 commits into
mainfrom
fix/schema-traversal-reaches-all-schemas

Conversation

@erraggy

@erraggy erraggy commented Jul 31, 2026

Copy link
Copy Markdown
Owner

validateSchema was called from exactly two places: the Request Body media type loop and components.schemas. Every schema in a response, a parameter or a header was therefore never validated — by any of the six rule families it runs, not only the OAS 3.2 one that exposed it. 🔍

Fixes #423

Reproduction

One 3.0.3 document, the same offending schema in four positions:

requestBody:
  content: {application/json: {schema: {type: object, xml: {name: p, nodeType: element}}}}
parameters:
  - {name: q, in: query, schema: {type: string, xml: {name: q, nodeType: attribute}}}
responses:
  "200":
    content: {application/json: {schema: {type: object, xml: {name: r, nodeType: element}}}}
components:
  schemas:
    Direct: {type: object, xml: {name: d, nodeType: element}}

Before: two of the four reported. After: all four.

What now gets reached

Response content, parameters (schema and content form), headers, components.mediaTypes, path-item-level parameters, Encoding Object headers, and Callback path items — at the operation, path item and components levels, under paths, webhooks and components.pathItems alike.

components.requestBodies is deliberately not among them: validateOAS3Components already reaches it through validateOAS3RequestBody, and walking it here reported every error in it twice. A test caught that, which is why every case asserts its schema is reported exactly once rather than merely present.

Callbacks are cycle-bounded. Callback → PathItem → Operation → Callback closes a loop that a parsed document cannot build but ValidateParsed accepts from a caller — the same reasoning as maxEncodingNestingDepth and the bound added in #419.

Request bodies now go through the same media type walk as every other position, so their 3.2 itemSchema is validated rather than only schema.

The converter had the matching half

detectOAS32SchemaFeatures was called for components.schemas and for mt.ItemSchema — never for mt.Schema, the ordinary schema every 3.0 document already has. So oastools convert -t 3.0.3 stayed silent on a 3.2 field in an inline media type schema.

The fixture now carries an inline occurrence, so this cannot reopen silently: it previously put nodeType and defaultMapping only under components.schemas, one of the two positions that already worked.

Google Maps stops being valid, correctly

"schema": { "type": "string", "enum": [0, 1, 2, 3, 4] }

Two of its parameters declare a string type with integer enum values, and the spec's own description says the values "range between 0 and 4" — so the type is the mistake, not the enum. That document read as valid only because parameter schemas were never looked at. The corpus expectation records the reason, not just the number.

All 8 corpus specs pass; no other count moved.

Order and cost

Sections are walked in map order rather than sorted. The passes that needed stable output (#411, #420) sorted the errors they appended, so a document with no findings paid nothing; sorting keys here would allocate a slice per map on every document — measured at double the cost, +1.0% versus +0.4% on MediumOAS3 — to fix a fraction of the problem. The validator's order is already unstable: the existing path walk alone produces five orderings in eight runs. Filed as #425.

baseline with the traversal
SmallOAS3 2211 2222
MediumOAS3 18905 19008
LargeOAS3 213072 214264
OAS 2.0 unchanged

Unlike the version gate in #411, this cost is real work — validating schemas that were never validated — not overhead.

Verification

22 positions covered, each asserting the schema is reported exactly once, which is also what catches a position being reached twice. Removing the traversal fails 9 subtests; removing the callback and encoding-header walks fails 3; the old request-body handling fails 2. The converter half fails 2 of its own.

make check green.

Release note

This changes validation outcomes, not just internals. Documents that passed at 3.0/3.1 may now report errors — five rule families beyond the 3.2 check now run in positions that were never examined. The findings are real defects that were previously invisible, but they will read as new failures in a CI pipeline, so this belongs near the top of the v1.59.0 notes rather than in a bugfix list.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@erraggy, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 20f11b4b-20cf-4c26-b6c8-ff7d9591cedf

📥 Commits

Reviewing files that changed from the base of the PR and between 18a8148 and 0f6ed22.

📒 Files selected for processing (1)
  • validator/schema_traversal.go
📝 Walkthrough

Walkthrough

The change expands OAS 3 schema traversal for validation and OAS 3.2 feature detection. It covers nested schemas in media types, parameters, responses, callbacks, encodings, webhooks, and component objects.

Changes

OAS 3 schema coverage

Layer / File(s) Summary
OAS 3.2 media type detection
converter/oas32_features.go, converter/oas32_features_test.go, parser/oas32_roundtrip_test.go, testdata/oas32-all-fields.*
Media type feature detection scans ordinary schemas. Inline XML metadata is added to fixtures and verified during conversion and JSON/YAML round trips.
Schema validation traversal
validator/schema_traversal.go, validator/oas3.go
Validation traverses schemas in operations, path items, responses, parameters, headers, media types, encodings, callbacks, webhooks, and component sections.
Schema validation coverage
validator/schema_traversal_test.go, internal/corpusutil/corpus.go
Tests cover supported schema locations, nested itemSchema values, duplicate references, cyclic callbacks, and the updated invalid GoogleMaps corpus expectation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main validator and converter change: validating schemas beyond the two previously covered positions.
Description check ✅ Passed The description directly explains the schema traversal gap, the implemented fix, validation coverage, converter changes, tests, performance impact, and linked issue.
Linked Issues check ✅ Passed The changes satisfy issue #423 by covering the required schema positions, converter paths, inline fixtures, exact-once traversal, cycle bounds, and performance measurement.
Out of Scope Changes check ✅ Passed The changes remain within scope because corpus updates, traversal tests, cycle protection, and performance data support the linked validator and converter objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/schema-traversal-reaches-all-schemas

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.71930% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.72%. Comparing base (98da867) to head (0f6ed22).

Files with missing lines Patch % Lines
validator/schema_traversal.go 86.79% 7 Missing and 7 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #426      +/-   ##
==========================================
+ Coverage   86.69%   86.72%   +0.03%     
==========================================
  Files         201      202       +1     
  Lines       29090    29202     +112     
==========================================
+ Hits        25219    25325     +106     
  Misses       2560     2560              
- Partials     1311     1317       +6     
Files with missing lines Coverage Δ
converter/oas32_features.go 80.44% <100.00%> (+0.17%) ⬆️
internal/corpusutil/corpus.go 87.32% <ø> (ø)
validator/oas3.go 86.36% <100.00%> (+0.13%) ⬆️
validator/schema_traversal.go 86.79% <86.79%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@validator/schema_traversal_test.go`:
- Around line 235-253: Extend the schema traversal test table with cases
covering each untested branch: components.mediaTypes via
validateOAS3ComponentSchemas, mt.ItemEncoding and mt.PrefixEncoding via
validateMediaTypeSchemas, nested enc.Encoding/item/prefix recursion via
validateEncodingSchemas, and callback path-item item.AdditionalOperations via
validatePathItemSchemas. Assert the complete expected path for each case,
including every distinct segment produced by the traversal.

In `@validator/schema_traversal.go`:
- Around line 49-65: The callback schema traversal must detect cyclic
caller-built documents before recursive branching. Update the validation flow
reached from ValidateParsed, including validateOperationSchemasAtDepth and
validateCallbackMapSchemas, to track visited *parser.PathItem pointers and stop
or report when a path item is revisited; retain maxCallbackNestingDepth as a
fail-safe for non-cyclic deep nesting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 22b6a648-567a-4bc2-bf4e-5d491cce9174

📥 Commits

Reviewing files that changed from the base of the PR and between 98da867 and 984667a.

📒 Files selected for processing (9)
  • converter/oas32_features.go
  • converter/oas32_features_test.go
  • internal/corpusutil/corpus.go
  • parser/oas32_roundtrip_test.go
  • testdata/oas32-all-fields.json
  • testdata/oas32-all-fields.yaml
  • validator/oas3.go
  • validator/schema_traversal.go
  • validator/schema_traversal_test.go

Comment thread validator/schema_traversal_test.go
Comment thread validator/schema_traversal.go
validateSchema was called from exactly two places: the Request Body media type
loop and `components.schemas`. Every schema in a response, a parameter or a
header was therefore never validated at all — by any of the six rule families
it runs, not only the OAS 3.2 one that exposed it.

Adds the traversal for response content, parameter, header and
components.mediaTypes schemas, at the operation, path item and components
levels. `components.requestBodies` is deliberately not among them:
validateOAS3Components already reaches it through validateOAS3RequestBody, and
walking it here reported every error in it twice. A test caught that.

The converter had the matching half. detectOAS32SchemaFeatures was called for
`components.schemas` and for `mt.ItemSchema`, never for `mt.Schema` — the
ordinary schema every 3.0 document already has.

Google Maps stops being valid, correctly. Two of its parameters declare
`"type": "string"` with `"enum": [0, 1, 2, 3, 4]`, and its own description says
the values "range between 0 and 4", so the type is the mistake. That spec read
as valid only because parameter schemas were never looked at. The corpus
expectation records the reason, not just the number.

Sections are walked in map order rather than sorted. The other passes that
needed stable output sorted the errors they appended, so a clean document paid
nothing; sorting keys here would allocate a slice per map on every document,
and measured at double the cost for a fraction of the benefit — the validator's
order is already unstable, five orderings in eight runs from the existing path
walk alone. Filed as #425.

Callbacks and Encoding Object headers are reached too. The callback walk
tracks visited path items rather than relying on a depth bound alone: a path
item whose operations each lead back to it branches, so the walk is
exponential in depth and never reaches the bound — two such operations ran
past forty seconds without returning. The bound stays as a fail-safe. The request body now goes
through the same media type walk as every other position, so its 3.2
itemSchema is validated rather than only its schema.

    SmallOAS3    2211 ->   2222
    MediumOAS3  18905 ->  19008
    LargeOAS3  213072 -> 214264
    OAS 2.0             unchanged

Twenty-two positions are covered by test, each asserting the schema is
reported exactly once, which is also what catches a position being reached
twice.

Fixes #423
@erraggy
erraggy force-pushed the fix/schema-traversal-reaches-all-schemas branch from 984667a to 18a8148 Compare July 31, 2026 23:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@validator/schema_traversal.go`:
- Around line 87-99: Update the fixed operation-method map in the schema
traversal logic to package scope and key it with the corresponding lowercase
constants from httputil instead of string literals. Use that shared map in the
loop around validateOperationSchemasAtDepth, while preserving the existing
operation coverage and AdditionalOperations handling.
- Around line 23-47: Make callback traversal state allocation lazy in
validateOAS3OperationSchemas and validateOperationSchemasAtDepth: avoid creating
a visited map for operations without callbacks, pass nil initially, and update
validateCallbackMapSchemas to initialize the map before its first
validatePathItemSchemas call when visited is nil. Preserve cycle tracking for
operations that do contain callbacks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f33661f7-5843-46b2-aed1-130753a9e3aa

📥 Commits

Reviewing files that changed from the base of the PR and between 984667a and 18a8148.

📒 Files selected for processing (9)
  • converter/oas32_features.go
  • converter/oas32_features_test.go
  • internal/corpusutil/corpus.go
  • parser/oas32_roundtrip_test.go
  • testdata/oas32-all-fields.json
  • testdata/oas32-all-fields.yaml
  • validator/oas3.go
  • validator/schema_traversal.go
  • validator/schema_traversal_test.go

Comment thread validator/schema_traversal.go
Comment thread validator/schema_traversal.go Outdated
…lazily

The Path Item operation list moves to package scope as a slice keyed by the
httputil method constants, per the "use constants" guideline. A slice rather
than a map so it is built once instead of per path item, and iterates in a
stable order.

The visited set exists only for the callback traversal, and most operations
carry no callbacks, so it is allocated only when one does. Nil is never
written: validatePathItemSchemas is reachable only through a callback.

No measurable allocation change — 2222 / 19008 / 214263 before and after, to
the digit. The compiler was already handling the unconditional make. Kept for
clarity, not for speed.
@erraggy
erraggy merged commit b5e63f0 into main Aug 1, 2026
16 checks passed
@erraggy
erraggy deleted the fix/schema-traversal-reaches-all-schemas branch August 1, 2026 01:26
erraggy added a commit that referenced this pull request Aug 1, 2026
Add a new commit instead. PRs merge with squash, so the branch collapses into
one commit anyway and amending buys nothing — while costing reviewers the diff
between what they reviewed and what changed, and unsticking comments anchored
to the old commit.

It also broke a push on #426. Amending diverged local from remote, so a
scheduled `git push` was rejected as a non-fast-forward; the steps after it
were newline-separated rather than chained with &&, so two threaded replies
posted anyway, each citing a commit that was not on the PR for four minutes.

Amend freely before the first push, never after.
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.

validator and converter: schema-level OAS 3.2 rules never reach response content or parameter schemas

1 participant