Skip to content

Add OpenApiValidationPlugin to validate API traffic against local OpenAPI specs - #2037

Open
waldekmastykarz wants to merge 11 commits into
dotnet:mainfrom
waldekmastykarz:validate-api-against-spec
Open

waldekmastykarz wants to merge 11 commits into
dotnet:mainfrom
waldekmastykarz:validate-api-against-spec

Conversation

@waldekmastykarz

Copy link
Copy Markdown
Collaborator

Closes #779

Adds OpenApiValidationPlugin, a reporting plugin that checks intercepted requests and responses against local OpenAPI spec files and reports any discrepancies.

Configuration

"openApiValidationPlugin": {
  "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v4.1.0/openapivalidationplugin.schema.json",
  "specFiles": [ "api.yaml" ],
  "specsFolder": "specs",
  "reportUnmatchedRequests": false
}
  • specFiles: list of spec files (JSON or YAML, OAS 3.x).
  • specsFolder: folder that's scanned recursively for .json, .yaml and .yml files.
  • At least one of specFiles or specsFolder is required.
  • reportUnmatchedRequests: if true, also report requests that don't match any spec's servers. Defaults to false.

Checks

  • Undocumented operations: paths, methods, status codes and content types that aren't in the spec. A request that matches a spec's server is reported if its path or method isn't documented.
  • Parameters:
    • Missing required path, query and header parameters.
    • Unknown query parameters.
    • Parameter values are checked for type and enum.
  • Bodies: request and response JSON bodies are validated with a built-in schema validator (no new dependencies). It supports type, nullable/null, required, properties, additionalProperties, items, enum, allOf, anyOf, oneOf and $ref.
  • Direction: readOnly properties aren't required in requests, and writeOnly properties aren't required in responses.
  • Mocked and simulated responses are validated too.

Output

  • Each request is logged live: a warning when it has issues, or a confirmation when it matches.
  • When recording stops, the plugin stores a report. It renders to Markdown and plain text, and also serializes to JSON.

Notes

  • The schema is in schemas/v4.1.0, in line with the version bump in Add per-mock rate to MockResponsePlugin #2027.
  • Tests: 14 new integration tests in OpenApiValidationPluginTests pass. CommandExitCodeTests and ProcessSmokeTests fail locally on main without this change too.
  • Manually tested with a detached proxy, MockResponsePlugin and PlainTextReporter.

…nAPI specs

Adds a reporting plugin that validates intercepted requests and responses
against local OpenAPI specs (specFiles and/or specsFolder) and reports
undocumented paths/methods/status codes/content types, parameter issues
and request/response body schema violations. Closes dotnet#779

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 10:01
@waldekmastykarz
waldekmastykarz requested a review from a team as a code owner October 31, 2026 10:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The depth-limit fallback treats unvalidated schemas as valid, allowing invalid bodies to pass, while unresolved matching and coercion defects can misclassify requests or abort report generation.

9 open issues
What changed in this PR

Adds OpenApiValidationPlugin to detect drift between intercepted API traffic and local OpenAPI specifications, addressing #779 through Dev Proxy’s existing reporting pipeline.

Changes:

  • Loads JSON/YAML specs from configured files or folders and checks operations, parameters, headers, and bodies.
  • Adds lightweight JSON schema validation and live discrepancy logging, with Markdown, plain-text, and JSON reports.
  • Adds configuration schema and integration tests for validation and reporting.
File Description
schemas/​v4.1.0/​openapivalidationplugin.schema.json Defines spec sources and unmatched-request reporting options.
DevProxy.Plugins/​Utils/​OpenApiSchemaValidator.cs Implements schema checks and parameter-value coercion.
DevProxy.Plugins/​Reporting/​OpenApiValidationPluginReport.cs Defines report models and Markdown/plain-text rendering.
DevProxy.Plugins/​Reporting/​OpenApiValidationPlugin.cs Loads specs, matches traffic, validates exchanges, and stores reports.
DevProxy.Integration.Tests/​OpenApiValidationPluginTests.cs Covers matching, validation errors, YAML loading, and report generation.

Review effort: Balanced

- Report schemas nested deeper than the supported maximum instead of treating them as valid
- Search all specs and matching paths for the requested method before reporting it as undocumented
- Recognize required deepObject query parameters
- Match operations against their effective (document-, path- or operation-level) servers
- Match server base paths and request paths case-sensitively and strictly on trailing slashes
- Decode array parameters according to their style and explode settings
- Reject non-finite numbers when coercing parameter values

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Schema validation lacks a total-work bound, allowing repeated references to trigger billions of synchronous evaluations and potentially stall response processing and report generation.

2 open issues
3 previously missed issues

In code that hasn't changed since last review

Medium severity Recognize exploded object query parameter properties

DevProxy.Plugins/​Reporting/​OpenApiValidationPlugin.cs:369

A required object query parameter using style: form and explode: true is serialized using its property names, not the parameter name. For example, filter with a status property is sent as ?status=active, but this lookup reports filter missing and the later unknown-parameter check reports status undocumented. Account for exploded object properties when checking presence and determining known query keys, even if object-value validation remains unsupported.

Medium severity Limit bracket-name exemption to deepObject parameters

DevProxy.Plugins/​Reporting/​OpenApiValidationPlugin.cs:431

The bracket-name exemption applies to every documented query parameter, not just deepObject parameters. For example, with the optional integer top parameter in the test spec, ?top[unexpected]=10 produces neither an unknown-parameter warning nor value validation. Restrict this exemption to parameters whose declared style is deepObject so unrelated bracketed query keys are still reported.

Medium severity Resolve external references relative to the spec file

DevProxy.Plugins/​Reporting/​OpenApiValidationPlugin.cs:750

Passing only the document text loses the source file URI needed to resolve relative external references. For example, a configured specs/openapi.yaml containing $ref: './schemas/User.yaml' cannot resolve that sibling schema relative to the spec file, so its constraints cannot be validated even though the spec is logged as loaded. Supply the file's base URI and resolve local references before accepting the document, or reject unresolved references explicitly.

9 resolved since last review

Review effort: Balanced

Memoize composed (allOf/anyOf/oneOf) subschema results per schema and
value, dedupe repeated allOf errors, and cap total evaluations as a
backstop so repeated references can't cause exponential validation work.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved validation defects reject valid query parameters, skip checks for default-port server URLs, and allow undocumented query keys, malformed JSON, and invalid values to pass.

1 open issue
5 previously missed issues

In code that hasn't changed since last review

Medium severity Recognize exploded form-object keys in query validation

DevProxy.Plugins/​Reporting/​OpenApiValidationPlugin.cs:372

A required object query parameter using style: form and explode: true is serialized as its property names: for example, a filter object with a status property is sent as ?status=active. This lookup reports filter missing, and the unknown-query check also reports status as undocumented. Recognize exploded form-object property keys for both presence and unknown-parameter checking, even if object-value schema validation remains skipped.

Medium severity Limit bracket-key exemption to deepObject parameters

DevProxy.Plugins/​Reporting/​OpenApiValidationPlugin.cs:431

The bracket-key exemption applies to every query parameter, regardless of its serialization style. With the existing optional integer top parameter, ?top[foo]=bar is neither validated as top nor reported as an unknown parameter, so an undocumented query key silently passes. Restrict this exemption to parameters explicitly declared with deepObject style.

Medium severity Parse JSON bodies when the media type has no schema

DevProxy.Plugins/​Reporting/​OpenApiValidationPlugin.cs:604

A documented application/json media type can omit its schema, but its payload still needs to be valid JSON. With content: { application/json: {} }, this early return accepts a malformed body such as { and logs that the response matches the spec. Parse JSON regardless of whether a schema is supplied; OpenApiSchemaValidator.Validate already handles a null schema.

Medium severity Normalize server authorities with explicit default ports

DevProxy.Plugins/​Reporting/​OpenApiValidationPlugin.cs:863

The server authority is compiled verbatim, while FindOperation matches against Uri.GetLeftPart, which omits an explicit default port. A valid server such as https://api.contoso.com:443/v1 therefore never matches requests to that endpoint, and validation is silently skipped. Normalize literal server authorities through Uri before compiling the regex so explicit default ports match the canonical request URL.

Medium severity Coerce parameter values using composed schema types

DevProxy.Plugins/​Utils/​OpenApiSchemaValidator.cs:141

A parameter schema can declare its type through composition without a top-level type, for example allOf: [{type: integer}]. This branch leaves ?count=42 as a JSON string, so the subsequent composition validation rejects a valid integer parameter. The same problem affects array item schemas declared through composition. Resolve applicable composed types, or evaluate schema-compatible coercion candidates, before validating parameter values.

2 resolved since last review

Review effort: Balanced

- Use unambiguous property paths so memoized results don't collide
- Recognize exploded form-object query parameters
- Exempt bracketed query keys only for deepObject parameters
- Report malformed JSON even when the media type has no schema
- Match server URLs with explicit default ports
- Coerce parameter values using types declared via composition

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unbounded traversal of repeated schema references in parameter type inference, array-item lookup, and property-name discovery can trigger billions of calls before the validation budget applies, stalling response processing.

3 open issues
1 resolved since last review

Review effort: Balanced

Type inference, item lookup and property discovery now traverse
allOf/anyOf/oneOf with a visited set, so repeated references and cycles
can't cause exponential work.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed to weigh unpublished correctness and performance findings involving rejected valid requests and responses and potentially stalled synchronous processing.

No open issues 🎉

3 resolved since last review

Review effort: Balanced

- When an allOf subschema with additionalProperties: false rejects a
  property declared in a sibling subschema, explain why in the message
- Skip validating JSON bodies larger than 5 MB to avoid stalling the
  proxy, and log that validation was skipped

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@waldekmastykarz

Copy link
Copy Markdown
Collaborator Author

Follow-up to the latest Copilot review, which said it needed a closer look but posted no inline threads. I addressed the likely findings in fb8bfb5:

  • allOf with additionalProperties: false: each allOf subschema is still validated on its own, as the spec requires. When a branch rejects a property that a sibling branch declares, the message now explains why: "The property is declared in a sibling allOf schema; additionalProperties: false applies per subschema". Test: HintsWhenAllOfBranchRejectsPropertyDeclaredInSibling.
  • Large bodies validated inline: JSON bodies over a fixed 5 MB are no longer parsed or validated. The proxy logs that validation was skipped and why. Test: SkipsValidatingLargeBodies.
  • oneOf discriminator: out of scope for this PR. It will be tracked in a follow-up issue.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Oversized request bodies are counted as validated despite being skipped, so reports can incorrectly claim full validation without disclosing unchecked bodies.

1 open issue

Review effort: Balanced

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@waldekmastykarz
waldekmastykarz requested a balanced review from Copilot October 31, 2026 13:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed because composed-schema error merging can stall synchronous processing, while parameter interpretation and depth-sensitive caching can incorrectly reject valid inputs.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

- Dedupe allOf errors with a hash set instead of a linear scan
- Include depth in the composed-schema memo key so depth-limited results
  aren't reused at shallower depths
- Accept parameter values that are valid as strings when the coerced
  value isn't, e.g. for schemas allowing both integers and strings

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@waldekmastykarz

Copy link
Copy Markdown
Collaborator Author

The latest Copilot review said "Needs a closer look" with no inline threads. I addressed the findings it hinted at in d2333a7:

  • Composed-schema error merging could stall processing: allOf error dedup used a linear List.Contains scan, which is O(n²) with many errors. It now uses a hash set.
  • Depth-sensitive caching could reject valid inputs: the composed-schema memo key didn't include depth, so a result that hit the depth limit could be reused for the same schema and value at a shallower depth. Depth is now part of the key. Test: DoesNotReuseDepthLimitedResultsAtShallowerDepths, which fails without the fix.
  • Parameter interpretation could reject valid inputs: values were only coerced to the first matching primitive type, so ?x=42 failed against anyOf: [{type: integer, enum: [1, 2]}, {type: string}]. When the coerced value fails, we now also try the raw string and accept the value if that's valid. Test: AcceptsParameterValuesValidAsStrings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed because unpublished findings span schema coercion, parameter parsing, reference traversal, and server-path matching, with multiple edge cases that can falsely reject valid requests.

No open issues 🎉

Review effort: Balanced

- Most specific matching path takes precedence; a missing method is reported as undocumented
- OpenAPI 3.1+ specs require null to be listed in enums
- Honor readOnly/writeOnly declared in allOf subschemas and references

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@waldekmastykarz

Copy link
Copy Markdown
Collaborator Author

Addressed the five findings from Copilot's review of #2042 (which builds on this branch) in 5bc98b1. Each one was first confirmed with a test.

  • Fixed: the most specific matching path takes precedence. A method it doesn't define is now reported as undocumented instead of falling through to a template.
  • Fixed: for OpenAPI 3.1+ specs, null must be listed in the enum. 3.0 specs stay lenient because of how nullable is mapped.
  • Fixed: the readOnly/writeOnly required exemption now honors declarations in allOf subschemas, including $refs.
  • Not changing: per-branch coercion for anyOf parameters that mix array and scalar branches, and for composed items with different item types. These schemas are rare and the extra complexity isn't justified.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed because array validation can stall synchronous response processing through quadratic error handling, alongside subtle decoding and schema-composition correctness risks.

No open issues 🎉

Review effort: Balanced

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@waldekmastykarz

Copy link
Copy Markdown
Collaborator Author

Copilot's review on 5bc98b1 had no inline threads, but its verdict flagged quadratic error handling in array validation. Confirmed: each allOf node copied the whole shared error list into its dedup set. With 20k invalid array items, validation took about 12.8 s. Fixed in c43ae1c by deduplicating only against errors reported for the current value. The same case now runs in milliseconds, and a regression test covers it. The verdict's other wording ('subtle decoding and schema-composition correctness risks') named nothing specific to act on.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed because OpenAPI reader errors can be silently discarded, allowing incomplete specifications to skip constraints and falsely report traffic as matching.

No open issues 🎉

Review effort: Balanced

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@waldekmastykarz

Copy link
Copy Markdown
Collaborator Author

Copilot's review on c43ae1c had no inline threads. Its verdict flagged OpenAPI reader errors being silently discarded. Confirmed: when a spec parsed with errors, the diagnostics were dropped. Fixed in e5c86f9: the plugin still loads the spec, but now logs a warning listing the reader errors, since parts of the spec may have been ignored. A test covers this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed because interacting edge cases in parameter coercion, schema caching, and media-type matching can incorrectly accept invalid requests or reject valid ones.

No open issues 🎉

Review effort: Balanced

@waldekmastykarz

Copy link
Copy Markdown
Collaborator Author

Copilot's review on e5c86f9 had no inline threads. Its verdict names areas but no concrete case:

  • Parameter coercion: this is the mixed array/scalar composition case we deliberately aren't changing (see the replies on Support discriminator for oneOf in OpenApiValidationPlugin #2042).
  • Schema caching: the memo lives for a single validation and is keyed by resolved schema, value path and depth, so cached results always apply to the same value.
  • Media-type matching: handles parameters, case-insensitivity, +json, type/* and */*, with tests covering these.

I couldn't find a failing case, so I'm leaving these as they are unless a specific example comes up.

This branch has not been deployed

No deployments
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.

Check if the API matches its local OpenAPI spec

2 participants