Skip to content

feat: oapi response validator#4283

Merged
tothandras merged 3 commits into
mainfrom
feat/oapi-codegen-response-validator
May 12, 2026
Merged

feat: oapi response validator#4283
tothandras merged 3 commits into
mainfrom
feat/oapi-codegen-response-validator

Conversation

@rolosp
Copy link
Copy Markdown
Collaborator

@rolosp rolosp commented May 4, 2026

Overview

Adds optional v3 response body validation against the OpenAPI spec. Violations are
logged as warnings; client responses are never affected.

Configured via server.responseValidation.mode:

  • off (default) — disabled
  • unstable — only routes marked x-unstable: true (~15 list/CRUD endpoints today)
  • all — every v3 route

The mode enum mirrors the existing AutoMigrate pattern in app/config/postgres.go.

Notes for reviewer

  • Validation buffers the response body in memory; unstable is the recommended
    rollout target. all is fine for staging but watch memory on large list pages.
  • mode: "off" is intentionally quoted in config.example.yaml — bare off is parsed
    as boolean false by YAML 1.1.
  • Violation logs include the kin-openapi error, which can echo offending response
    fields. Acceptable for observability but worth knowing.
  • v1 and request-side filtering aren't touched (out of scope)

Summary by CodeRabbit

  • New Features

    • Configurable API response validation with modes: off (default), unstable (validates only unstable-marked operations), and all. Validation can be scoped per-route and optionally skipped via a route filter; validation failures are logged as warnings while client responses remain unchanged.
  • Configuration

    • New server.responseValidation.mode setting (default: "off").
  • Tests

    • Added tests covering valid vs invalid responses and skipping validation via the route filter.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds configurable OpenAPI response validation: new server config and modes (off, unstable, all), middleware support for a per-route RouteFilterHook to skip validation, wiring through server/CLI, and tests covering violation, clean response, and route-filter behavior.

Changes

Response Validation

Layer / File(s) Summary
Configuration Types
app/config/server.go
Adds ResponseValidationConfig, ResponseValidationMode (off, unstable, all), Enabled()/Validate() methods, integrates into ServerConfig, and sets default to off.
Server Config Wiring
cmd/server/main.go, openmeter/server/server.go, api/v3/server/server.go
Propagates conf.Server.ResponseValidation into router/server configs. api/v3/server/server.go's Config struct gains ResponseValidation field and is passed into server construction.
Response Validation Middleware
api/v3/oasmiddleware/validator.go
ValidateResponseOption adds RouteFilterHook func(*routers.Route) bool. After route lookup, if the hook exists and returns false, middleware serves the request immediately, skipping response wrapping and openapi3filter.ValidateResponse.
Route Filter Logic
api/v3/server/server.go
Adds buildResponseValidationRouteFilter(cfg) used when validation enabled; in unstable mode it enables validation only for operations with x-unstable: true (handles json.RawMessage and bool).
Tests & Examples
api/v3/oasmiddleware/validator_test.go, app/config/config_test.go, config.example.yaml
Adds tests: violation, clean response, and route-filter skipping. Config test updated to expect ResponseValidation.Mode: off. Example YAML documents server.responseValidation.mode: "off".
sequenceDiagram
    participant Client as Client
    participant Middleware as Middleware
    participant Router as Router
    participant Handler as Handler
    participant Validator as Validator
    participant ErrorHook as ErrorHook

    Client->>Middleware: send HTTP request
    Middleware->>Handler: invoke handler (buffer response)
    Handler-->>Middleware: buffered status + body
    Middleware->>Router: find matching route
    Router-->>Middleware: route
    Middleware->>Middleware: call RouteFilterHook(route)
    alt RouteFilterHook returns false
        Middleware-->>Client: write original buffered response
    else
        Middleware->>Validator: validate buffered response against route
        alt validation fails
            Validator-->>ErrorHook: report validation error
            ErrorHook-->>Middleware: hook executed
            Middleware-->>Client: write original buffered response
        else
            Middleware-->>Client: write original buffered response
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

release-note/feature

Suggested reviewers

  • tothandras
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: oapi response validator' clearly and concisely describes the main change: adding an optional OpenAPI response body validator with configurable modes (off, unstable, all) across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oapi-codegen-response-validator

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@rolosp rolosp changed the title feat: oapi codegen response validator feat: oapi response validator May 4, 2026
@rolosp rolosp marked this pull request as ready for review May 4, 2026 14:36
@rolosp rolosp requested a review from a team as a code owner May 4, 2026 14:36
@rolosp rolosp added the release-note/misc Miscellaneous changes label May 4, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/v3/server/server.go (1)

70-77: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast on invalid response-validation modes.

This new public knob is added to Config, but it still bypasses Config.Validate(). A typo that reaches this constructor can be treated as enabled downstream instead of rejecting the config.

🔧 Suggested fix
 func (c *Config) Validate() error {
 	var errs []error
+
+	if err := c.ResponseValidation.Mode.Validate(); err != nil {
+		errs = append(errs, err)
+	}
 
 	if c.BaseURL == "" {
 		errs = append(errs, errors.New("base URL is required"))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/server/server.go` around lines 70 - 77, The ResponseValidation field
on Config is currently accepted without being checked; ensure the config is
validated at construction by calling Config.Validate() (or equivalent validation
helper) from the server constructor (e.g., wherever a new server is created) and
return an error if validation fails so typos in ResponseValidation are rejected;
specifically, add a validation call that inspects Config.ResponseValidation via
Config.Validate() before using it in server initialization.
🧹 Nitpick comments (1)
api/v3/oasmiddleware/validator_test.go (1)

23-31: Swap to t.Context() here and at lines 62 and 98.

These test setups should use the test context instead of context.Background(). It ties the router lifecycle to the test harness and matches the pattern already used throughout the codebase.

Suggested refactor
-	router, err := oasmiddleware.NewValidationRouter(context.Background(), swagger, &oasmiddleware.ValidationRouterOpts{
+	router, err := oasmiddleware.NewValidationRouter(t.Context(), swagger, &oasmiddleware.ValidationRouterOpts{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/oasmiddleware/validator_test.go` around lines 23 - 31, Replace
context.Background() with the test's cancellable context by using t.Context()
when calling NewValidationRouter in this test; specifically update the
NewValidationRouter invocation that currently passes context.Background() (and
the two other occurrences in the same test where context.Background() is used)
so the router lifecycle is tied to the test harness. Ensure you only change the
context argument passed into oasmiddleware.NewValidationRouter and leave the
rest of the ValidationRouterOpts (e.g., DeleteServers) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@api/v3/server/server.go`:
- Around line 70-77: The ResponseValidation field on Config is currently
accepted without being checked; ensure the config is validated at construction
by calling Config.Validate() (or equivalent validation helper) from the server
constructor (e.g., wherever a new server is created) and return an error if
validation fails so typos in ResponseValidation are rejected; specifically, add
a validation call that inspects Config.ResponseValidation via Config.Validate()
before using it in server initialization.

---

Nitpick comments:
In `@api/v3/oasmiddleware/validator_test.go`:
- Around line 23-31: Replace context.Background() with the test's cancellable
context by using t.Context() when calling NewValidationRouter in this test;
specifically update the NewValidationRouter invocation that currently passes
context.Background() (and the two other occurrences in the same test where
context.Background() is used) so the router lifecycle is tied to the test
harness. Ensure you only change the context argument passed into
oasmiddleware.NewValidationRouter and leave the rest of the ValidationRouterOpts
(e.g., DeleteServers) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 60d78ef9-3cdd-4f53-97b8-98cd8d76ff64

📥 Commits

Reviewing files that changed from the base of the PR and between acff67d and b4020a0.

📒 Files selected for processing (8)
  • api/v3/oasmiddleware/validator.go
  • api/v3/oasmiddleware/validator_test.go
  • api/v3/server/server.go
  • app/config/config_test.go
  • app/config/server.go
  • cmd/server/main.go
  • config.example.yaml
  • openmeter/server/server.go

@rolosp
Copy link
Copy Markdown
Collaborator Author

rolosp commented May 5, 2026

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)

api/v3/server/server.go (1)> 70-77: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast on invalid response-validation modes.
This new public knob is added to Config, but it still bypasses Config.Validate(). A typo that reaches this constructor can be treated as enabled downstream instead of rejecting the config.

🔧 Suggested fix

 func (c *Config) Validate() error {
 	var errs []error
+
+	if err := c.ResponseValidation.Mode.Validate(); err != nil {
+		errs = append(errs, err)
+	}
 
 	if c.BaseURL == "" {
 		errs = append(errs, errors.New("base URL is required"))
 	}

🤖 Prompt for AI Agents

Verify each finding against the current code and only fix it if needed.

In `@api/v3/server/server.go` around lines 70 - 77, The ResponseValidation field
on Config is currently accepted without being checked; ensure the config is
validated at construction by calling Config.Validate() (or equivalent validation
helper) from the server constructor (e.g., wherever a new server is created) and
return an error if validation fails so typos in ResponseValidation are rejected;
specifically, add a validation call that inspects Config.ResponseValidation via
Config.Validate() before using it in server initialization.

🧹 Nitpick comments (1)

api/v3/oasmiddleware/validator_test.go (1)> 23-31: Swap to t.Context() here and at lines 62 and 98.

These test setups should use the test context instead of context.Background(). It ties the router lifecycle to the test harness and matches the pattern already used throughout the codebase.

Suggested refactor

-	router, err := oasmiddleware.NewValidationRouter(context.Background(), swagger, &oasmiddleware.ValidationRouterOpts{
+	router, err := oasmiddleware.NewValidationRouter(t.Context(), swagger, &oasmiddleware.ValidationRouterOpts{

🤖 Prompt for AI Agents

Verify each finding against the current code and only fix it if needed.

In `@api/v3/oasmiddleware/validator_test.go` around lines 23 - 31, Replace
context.Background() with the test's cancellable context by using t.Context()
when calling NewValidationRouter in this test; specifically update the
NewValidationRouter invocation that currently passes context.Background() (and
the two other occurrences in the same test where context.Background() is used)
so the router lifecycle is tied to the test harness. Ensure you only change the
context argument passed into oasmiddleware.NewValidationRouter and leave the
rest of the ValidationRouterOpts (e.g., DeleteServers) unchanged.

🤖 Prompt for all review comments with AI agents

Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@api/v3/server/server.go`:
- Around line 70-77: The ResponseValidation field on Config is currently
accepted without being checked; ensure the config is validated at construction
by calling Config.Validate() (or equivalent validation helper) from the server
constructor (e.g., wherever a new server is created) and return an error if
validation fails so typos in ResponseValidation are rejected; specifically, add
a validation call that inspects Config.ResponseValidation via Config.Validate()
before using it in server initialization.

---

Nitpick comments:
In `@api/v3/oasmiddleware/validator_test.go`:
- Around line 23-31: Replace context.Background() with the test's cancellable
context by using t.Context() when calling NewValidationRouter in this test;
specifically update the NewValidationRouter invocation that currently passes
context.Background() (and the two other occurrences in the same test where
context.Background() is used) so the router lifecycle is tied to the test
harness. Ensure you only change the context argument passed into
oasmiddleware.NewValidationRouter and leave the rest of the ValidationRouterOpts
(e.g., DeleteServers) unchanged.

ℹ️ Review info

No change needed. Configuration.Validate() is already
called at startup in cmd/server/main.go:65, which recursively runs
Server.Validate() (app/config/config.go:163), which in turn runs
ResponseValidation.Mode.Validate() (app/config/server.go:68). A typo
like mode: unstabel is rejected before the server is constructed,
with server: invalid response validation mode (allowed: off,
unstable, all). Adding another check at the v3 constructor would be
redundant.

Comment thread api/v3/oasmiddleware/validator.go Outdated
@rolosp rolosp force-pushed the feat/oapi-codegen-response-validator branch from ddab792 to 43a4ecb Compare May 5, 2026 13:54
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@api/v3/server/server.go`:
- Around line 393-395: The ResponseValidationErrorHook currently logs the raw
validation error (err) via slog.WarnContext which can leak sensitive response
fields; update the handler for ResponseValidationErrorHook to log only a
sanitized summary (HTTP method, path, response status or a short error
category/string) at warning level and remove slog.Any("error", err) from the
warn entry, and if you need full details send the original err to
slog.DebugContext or behind an explicit opt-in/redaction helper so raw error
contents are not written to standard warning logs.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d6df4e6d-29d4-4571-9fce-754c8646a304

📥 Commits

Reviewing files that changed from the base of the PR and between ddab792 and 43a4ecb.

📒 Files selected for processing (8)
  • api/v3/oasmiddleware/validator.go
  • api/v3/oasmiddleware/validator_test.go
  • api/v3/server/server.go
  • app/config/config_test.go
  • app/config/server.go
  • cmd/server/main.go
  • config.example.yaml
  • openmeter/server/server.go
✅ Files skipped from review due to trivial changes (2)
  • app/config/config_test.go
  • api/v3/oasmiddleware/validator_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/config/server.go
  • config.example.yaml
  • openmeter/server/server.go
  • api/v3/oasmiddleware/validator.go

Comment thread api/v3/server/server.go
@rolosp rolosp requested a review from mark-vass-konghq May 5, 2026 14:37
@rolosp rolosp force-pushed the feat/oapi-codegen-response-validator branch from 43a4ecb to 0f8dc71 Compare May 6, 2026 08:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
api/v3/server/server.go (1)

393-395: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Previous review note still applies: raw error logged here may leak sensitive response data.

slog.Any("error", err) from openapi3filter.ValidateResponse can include offending response field values. The past review comment flagged this and suggested logging only method/path/status + error type at warn level, with raw details behind debug.

🤖 Prompt for 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.

In `@api/v3/server/server.go` around lines 393 - 395, The
ResponseValidationErrorHook currently logs the raw error (slog.Any("error",
err)) which can leak sensitive response fields from
openapi3filter.ValidateResponse; change the hook to log only method, path and
response status plus the error type at warn (e.g., slog.String("error_type",
fmt.Sprintf("%T", err))) and move the full/error details into a debug log entry
(slog.DebugContext) so raw response contents are not emitted at warn; update the
ResponseValidationErrorHook handler to use r.Method, r.URL.Path, r.Context(),
the response status if available, and fmt.Sprintf("%T", err) for the warn log
and log the original err at debug level.
🧹 Nitpick comments (1)
api/v3/server/server.go (1)

418-440: 💤 Low value

buildResponseValidationRouteFilter — a switch would make the intent clearer.

The current if cfg.Mode != config.ResponseValidationModeUnstable { return nil } is correct today (only two enabled modes), but it implicitly relies on "anything non-unstable gets nil" which could silently misbehave if a new enabled mode is added later.

A switch with explicit cases is more readable and easier to extend:

♻️ Suggested refactor
 func buildResponseValidationRouteFilter(cfg config.ResponseValidationConfig) func(*routers.Route) bool {
-	if cfg.Mode != config.ResponseValidationModeUnstable {
-		return nil
-	}
-	return func(route *routers.Route) bool {
+	switch cfg.Mode {
+	case config.ResponseValidationModeAll:
+		return nil
+	case config.ResponseValidationModeUnstable:
+		return func(route *routers.Route) bool {
+			if route.Operation == nil {
+				return false
+			}
+			extVal, ok := route.Operation.Extensions["x-unstable"]
+			if !ok {
+				return false
+			}
+			switch v := extVal.(type) {
+			case json.RawMessage:
+				var b bool
+				return json.Unmarshal(v, &b) == nil && b
+			case bool:
+				return v
+			default:
+				return false
+			}
+		}
+	default:
+		return nil
+	}
+}
+
+// (remove old closing brace below)
-		if route.Operation == nil {
-			return false
-		}
-		extVal, ok := route.Operation.Extensions["x-unstable"]
-		if !ok {
-			return false
-		}
-		switch v := extVal.(type) {
-		case json.RawMessage:
-			var b bool
-			return json.Unmarshal(v, &b) == nil && b
-		case bool:
-			return v
-		default:
-			return false
-		}
-	}
 }
🤖 Prompt for 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.

In `@api/v3/server/server.go` around lines 418 - 440, Replace the initial if-check
in buildResponseValidationRouteFilter with an explicit switch on cfg.Mode (e.g.,
switch cfg.Mode { case config.ResponseValidationModeUnstable: /* return closure
*/ default: return nil }) so only the explicit enabled case(s) return the
route-filter closure; keep the existing closure logic (checking route.Operation,
inspecting route.Operation.Extensions["x-unstable"] and the json.RawMessage/bool
handling) unchanged and place it under the ResponseValidationModeUnstable case
to make intent explicit and easier to extend.
🤖 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.

Duplicate comments:
In `@api/v3/server/server.go`:
- Around line 393-395: The ResponseValidationErrorHook currently logs the raw
error (slog.Any("error", err)) which can leak sensitive response fields from
openapi3filter.ValidateResponse; change the hook to log only method, path and
response status plus the error type at warn (e.g., slog.String("error_type",
fmt.Sprintf("%T", err))) and move the full/error details into a debug log entry
(slog.DebugContext) so raw response contents are not emitted at warn; update the
ResponseValidationErrorHook handler to use r.Method, r.URL.Path, r.Context(),
the response status if available, and fmt.Sprintf("%T", err) for the warn log
and log the original err at debug level.

---

Nitpick comments:
In `@api/v3/server/server.go`:
- Around line 418-440: Replace the initial if-check in
buildResponseValidationRouteFilter with an explicit switch on cfg.Mode (e.g.,
switch cfg.Mode { case config.ResponseValidationModeUnstable: /* return closure
*/ default: return nil }) so only the explicit enabled case(s) return the
route-filter closure; keep the existing closure logic (checking route.Operation,
inspecting route.Operation.Extensions["x-unstable"] and the json.RawMessage/bool
handling) unchanged and place it under the ResponseValidationModeUnstable case
to make intent explicit and easier to extend.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2fa14188-ec5a-4feb-96f5-ad722f30e4ef

📥 Commits

Reviewing files that changed from the base of the PR and between 43a4ecb and 0f8dc71.

📒 Files selected for processing (8)
  • api/v3/oasmiddleware/validator.go
  • api/v3/oasmiddleware/validator_test.go
  • api/v3/server/server.go
  • app/config/config_test.go
  • app/config/server.go
  • cmd/server/main.go
  • config.example.yaml
  • openmeter/server/server.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/v3/oasmiddleware/validator.go

@rolosp rolosp force-pushed the feat/oapi-codegen-response-validator branch from 0f8dc71 to 2e720c1 Compare May 7, 2026 10:13
@tothandras tothandras force-pushed the feat/oapi-codegen-response-validator branch from 2e720c1 to f1f99a7 Compare May 11, 2026 15:34
tothandras
tothandras previously approved these changes May 11, 2026
@tothandras tothandras enabled auto-merge (squash) May 12, 2026 08:17
@tothandras tothandras merged commit 6fc8695 into main May 12, 2026
26 checks passed
@tothandras tothandras deleted the feat/oapi-codegen-response-validator branch May 12, 2026 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/misc Miscellaneous changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants