Skip to content

Add swagger validator package#1735

Open
notaphplover wants to merge 21 commits intomainfrom
feat/add-swagger-validator-package
Open

Add swagger validator package#1735
notaphplover wants to merge 21 commits intomainfrom
feat/add-swagger-validator-package

Conversation

@notaphplover
Copy link
Copy Markdown
Member

@notaphplover notaphplover commented Apr 4, 2026

Context

See #1701.

Summary by CodeRabbit

  • New Features

    • OpenAPI-driven request body validation (v3.1 & v3.2): shared @ValidatedBody decorator and versioned OpenApiValidationPipe using Ajv; new package published.
    • Public API expanded to expose OpenAPI metadata and controller parameter metadata for integration and tooling.
  • Documentation

    • New blog post, guides, and API docs with examples and quick-start instructions.
  • Tests

    • Extensive unit and integration tests covering validation flows and error handling.
  • Chores

    • Package/configuration and tooling files added for the new package.

Add @inversifyjs/http-openapi-validation with:
- Validate parameter decorator
- OpenApiValidationPipe for v3.1 and v3.2
- Ajv-based schema validation with format support
- Content-type resolution from request headers
- Subpath exports for v3.1 (default) and v3.2
Add end-to-end integration tests for:
- v3.1 body validation (valid/invalid requests)
- v3.2 body validation (valid/invalid requests)
- Content-type fallback (single content type)
- Content-type ambiguity error (multiple types)
Add changesets for:
- @inversifyjs/http-core (minor): new public exports
- @inversifyjs/http-open-api (minor): getter + metadata exports
- @inversifyjs/http-openapi-validation (minor): new package
- add validation-docs/openapi/ with introduction and API pages
- add code examples for OpenApiValidationPipe and Validate
- add integration test for OpenAPI validation code example
- add http-open-api, http-openapi-validation devDeps to examples
@notaphplover notaphplover self-assigned this Apr 4, 2026
@notaphplover notaphplover requested a review from adrianmjim as a code owner April 4, 2026 16:25
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 4, 2026

🦋 Changeset detected

Latest commit: cb350a7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 15 packages
Name Type
@inversifyjs/http-core Minor
@inversifyjs/http-open-api Minor
@inversifyjs/http-openapi-validation Minor
@inversifyjs/http-better-auth Minor
@inversifyjs/http-express-v4 Minor
@inversifyjs/http-express Minor
@inversifyjs/http-fastify Minor
@inversifyjs/http-hono Minor
@inversifyjs/http-sse Patch
@inversifyjs/http-uwebsockets Minor
@inversifyjs/http-validation Minor
@inversifyjs/ajv-validation Minor
@inversifyjs/class-validation Minor
@inversifyjs/standard-schema-validation Minor
@inversifyjs/validation-common Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 305b08a8-c89d-480e-89d1-91a00efbb7ef

📥 Commits

Reviewing files that changed from the base of the PR and between 4e35f90 and cb350a7.

📒 Files selected for processing (2)
  • packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot1/OpenApiValidationPipe.int.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.int.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.int.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot1/OpenApiValidationPipe.int.spec.ts

📝 Walkthrough

Walkthrough

Adds a new package @inversifyjs/http-openapi-validation (shared @ValidatedBody() decorator + versioned OpenApiValidationPipe), exposes controller parameter metadata from @inversifyjs/http-core, and adds openApiObject getter plus OpenAPI metadata exports in @inversifyjs/http-open-api. Includes tests, docs, examples, and changesets.

Changes

Cohort / File(s) Summary
Changesets & Config
/.changeset/*, .changeset/config.json
Added changeset entries for three packages and added @inversifyjs/http-openapi-validation to the fixed group.
New Package: openapi-validation (manifest & tooling)
packages/framework/http/libraries/openapi-validation/package.json, tsconfig*.json, vitest.config.mjs, eslint.config.mjs, prettier.config.mjs, stryker.config.mjs, .lintstagedrc.json, .gitignore
New package manifest and build/test/lint tooling files.
Public API exports
@inversifyjs/http-core
packages/framework/http/libraries/core/src/index.ts
@inversifyjs/http-open-api
packages/framework/http/libraries/open-api/src/index.ts, .../src/v3Dot2.ts
Exported getControllerMethodParameterMetadataList and ControllerMethodParameterMetadata from http-core; exported controllerOpenApiMetadataReflectKey and ControllerOpenApiMetadata from open-api v3.1/v3.2 entrypoints.
SwaggerUiProvider getter & tests
.../open-api/.../SwaggerUiProvider.ts, .../SwaggerUiProvider.spec.ts
Added openApiObject read-only getter in v3.1 and v3.2 providers and unit tests asserting it returns the internal OpenAPI object.
Validation decorator & metadata
packages/.../openapi-validation/src/metadata/...
ValidatedBody.ts, setValidateMetadata.ts, openApiValidationMetadataReflectKey.ts and specs
Added @ValidatedBody() parameter decorator, reflect-metadata key and setter action to mark parameters and package request context (body/method/url/content-type).
Validation models & symbols
.../validation/models/*
Added BodyValidationInputParam, ValidationHandler, DiscriminatorValidationHandlerPair, ValidationInputParam alias, validatedInputParamBodyType symbol, and per-version SCHEMA_ID constants.
Core validation utilities (v3.1 & v3.2)
.../validation/calculations/* and tests
Added utilities: getPath, getMimeType, getPathItemObject, getOperationObject, getRequestBodyObject, inferContentType, handleBodyValidation, and buildCompositeValidationHandler with comprehensive unit tests for both v3.1 and v3.2.
Validation pipes & integration tests
.../validation/pipes/v3Dot1/OpenApiValidationPipe.ts, .../v3Dot2/OpenApiValidationPipe.ts and integration specs
Added OpenApiValidationPipe implementations (v3.1 & v3.2) that lazily initialize Ajv, inspect parameter metadata, and validate request bodies; includes integration tests covering content-type resolution and error mapping.
Examples & docs
packages/docs/.../validation-docs/openapi/*, blog post, examples under packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/*
Added documentation pages and a blog post, plus runnable examples and integration specs demonstrating @ValidatedBody() and OpenApiValidationPipe.
Design & spec artifacts
openspec/changes/*
Updated design, proposal, spec, and task checklist documenting the decorator/pipe approach and public API layout.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Decorator as `@ValidatedBody` (handler)
  participant Controller as Controller (parameter)
  participant Pipe as OpenApiValidationPipe
  participant OpenApi as SwaggerUiProvider.openApiObject
  participant Ajv as Ajv Validator

  Client->>Decorator: HTTP request (body + headers)
  Decorator-->>Controller: BodyValidationInputParam (body, method, url, contentType, type)
  Controller->>Pipe: execute(awaited input, metadata)
  Pipe->>OpenApi: lookup operation/requestBody (path + method)
  alt content-type missing
    Pipe->>OpenApi: infer content-type from operation.requestBody.content
  end
  Pipe->>Ajv: getSchema(jsonPointer)
  alt validator found
    Pipe->>Ajv: validate(input.body)
    alt valid
      Pipe-->>Controller: return validated body
    else invalid
      Pipe-->>Controller: throw InversifyValidationError
    end
  else validator missing
    Pipe-->>Controller: throw InversifyValidationError (missing schema)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • adrianmjim

Poem

🐰 I hopped through specs and tests today,

gathered schemas on my way.
With ValidatedBody snug and spry,
Ajv keeps requests from going awry.
A carrot-coded cheer—validation, hi! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Add swagger validator package' is vague and generic. It lacks specificity about what exactly is being added (missing that this introduces OpenAPI-driven request body validation via a new @inversifyjs/http-openapi-validation package with @ValidatedBody decorator and OpenApiValidationPipe). Revise the title to be more specific and descriptive, such as: 'Add @inversifyjs/http-openapi-validation package with OpenAPI-driven body validation' or 'Introduce OpenAPI body validation with @ValidatedBody decorator and OpenApiValidationPipe'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ 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 feat/add-swagger-validator-package

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.

Copy link
Copy Markdown

@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: 12

🧹 Nitpick comments (4)
packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot1/getControllerOpenApiMetadata.ts (1)

6-13: Consider adding JSDoc documentation for this public API.

This function is part of the package's public API surface. Adding JSDoc documentation would improve developer experience by providing inline documentation about the function's purpose, parameters, and return value.

📝 Suggested JSDoc
+/**
+ * Retrieves OpenAPI metadata for a controller.
+ *
+ * `@param` target - The controller class to retrieve metadata from
+ * `@returns` The controller's OpenAPI metadata if present, otherwise undefined
+ */
 export function getControllerOpenApiMetadata(
   target: object,
 ): ControllerOpenApiMetadata | undefined {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot1/getControllerOpenApiMetadata.ts`
around lines 6 - 13, Add JSDoc for the exported getControllerOpenApiMetadata
function: document its purpose (retrieves controller OpenAPI metadata from the
target via reflect metadata), describe the parameter `target: object` (the
controller/class whose metadata is read), mention the return type
`ControllerOpenApiMetadata | undefined` and what undefined means (no metadata
found), and reference the internal helpers/keys used (`getOwnReflectMetadata`,
`controllerOpenApiMetadataReflectKey`) for clarity; place the JSDoc immediately
above the getControllerOpenApiMetadata declaration and keep it concise and
consistent with other public API docs in the package.
packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot2/getControllerOpenApiMetadata.ts (1)

6-13: Consider adding JSDoc documentation for this public API.

This function is part of the package's public API surface. Adding JSDoc documentation would improve developer experience and maintain consistency with the v3Dot1 counterpart.

📝 Suggested JSDoc
+/**
+ * Retrieves OpenAPI metadata for a controller.
+ *
+ * `@param` target - The controller class to retrieve metadata from
+ * `@returns` The controller's OpenAPI metadata if present, otherwise undefined
+ */
 export function getControllerOpenApiMetadata(
   target: object,
 ): ControllerOpenApiMetadata | undefined {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot2/getControllerOpenApiMetadata.ts`
around lines 6 - 13, Add a JSDoc comment for the public function
getControllerOpenApiMetadata describing its purpose (retrieves controller
OpenAPI metadata via reflect metadata), document the parameter target (object)
and the return type ControllerOpenApiMetadata | undefined, and mention that it
uses getOwnReflectMetadata with controllerOpenApiMetadataReflectKey; follow the
style and wording used in the v3Dot1 counterpart to keep consistency across
versions.
packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot1/getControllerOpenApiMetadata.spec.ts (1)

11-70: Align this spec with the repository’s required test structure/fixtures.

The assertions are good, but the unit test shape should follow the required 4-layer describe hierarchy and fixture-class pattern instead of ad-hoc let fixtures in beforeAll.

As per coding guidelines, **/*.spec.ts must use the four-layer describe structure (Class → Method → Input → Flow scopes), and **/*.{spec,int.spec}.ts should use reusable fixture classes with static methods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot1/getControllerOpenApiMetadata.spec.ts`
around lines 11 - 70, Refactor the spec for getControllerOpenApiMetadata to
follow the repository’s 4-layer describe hierarchy (Class → Method → Input →
Flow) and replace ad-hoc let fixtures with a reusable fixture class that exposes
static factory methods; specifically, wrap tests in nested describes for the
target class, the getControllerOpenApiMetadata method, the input case (metadata
present vs undefined), and the execution flow, and create a fixture class (e.g.,
ControllerFixture) with static methods to produce targetFixture and
metadataFixture; update test bodies to call
vitest.mocked(getOwnReflectMetadata).mockReturnValueOnce(...) and assert calls
and returns against controllerOpenApiMetadataReflectKey as before, but using the
new fixture class and describe structure to comply with the required pattern.
packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.ts (1)

35-233: Consider extracting shared logic between v3.1 and v3.2 implementations.

The v3Dot1 and v3Dot2 OpenApiValidationPipe implementations are nearly identical, differing only in import paths, type names, and SCHEMA_ID. A generic base class or shared utility functions could reduce duplication and simplify maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.ts`
around lines 35 - 233, The two implementations of OpenApiValidationPipe (v3Dot1
and v3Dot2) duplicate logic; extract the common behavior into a shared base
class or utilities and have the version-specific classes only supply differences
(types, import SCHEMA_ID, and content-type mapping). Concretely, create a
BaseOpenApiValidationPipe that contains execute, `#getOrInitAjv`, and
`#resolveContentType` logic using generic type parameters or abstract getters for
the OpenAPI object type and SCHEMA_ID, then refactor the v3Dot2
OpenApiValidationPipe to extend BaseOpenApiValidationPipe and override/implement
only the version-specific pieces (e.g., providing this.#openApiObject type,
SCHEMA_ID constant, and any type aliases), doing the same for v3Dot1 so both
implementations reuse the shared logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/config.json:
- Line 12: The package "@inversifyjs/http-openapi-validation" in the fixed group
has version 0.1.0 which mismatches the group's 3.0.0; open the package.json for
the `@inversifyjs/http-openapi-validation` package and change the "version" field
from "0.1.0" to "3.0.0" so it matches the other fixed-group packages referenced
in .changeset/config.json, then verify the package name
"@inversifyjs/http-openapi-validation" is unchanged and commit the updated
package.json.

In
`@packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiIntroduction.int.spec.ts`:
- Around line 15-16: The afterAll teardown assumes server was set by beforeAll
and will throw if beforeAll failed early; update the afterAll blocks that
reference server (e.g., the server variable used alongside beforeAll/afterAll)
to guard the teardown by checking the server is truthy before calling its
close/shutdown method (e.g., if (server) await server.close()), and apply the
same guard to the other afterAll at the other occurrence (lines referenced
around 47-49) so teardown does not raise a secondary error when setup failed.

In
`@packages/framework/http/libraries/open-api/src/openApi/services/v3Dot1/SwaggerUiProvider.spec.ts`:
- Around line 71-87: The test suite for SwaggerUiProvider.openApiObject must
follow the Class → Method → Input → Flow describe structure and use the 'when
called, and [condition]' naming pattern; update the nested describes so the
outermost is SwaggerUiProvider, the next targets .openApiObject, then an
input-level describe (e.g., 'with optionsFixture'), and the flow-level describe
should be 'when called, and [condition]' (or simply 'when called, and no special
options' if nothing else), and keep the existing assertions using result and
optionsFixture.api.openApiObject; ensure the describe strings reference
SwaggerUiProvider and .openApiObject so the test name aligns with project
conventions.

In
`@packages/framework/http/libraries/open-api/src/openApi/services/v3Dot1/SwaggerUiProvider.ts`:
- Around line 70-72: The openApiObject getter currently returns
this.#options.api.openApiObject without verifying initialization; add a guard
using the provider's internal flag (this.#provided) to throw a clear error if
openApiObject is accessed before provide() has been called. Update the
SwaggerUiProvider class's openApiObject getter to check this.#provided and throw
a descriptive Error (e.g., "OpenAPI object not provided — call provide() first")
when false, otherwise return this.#options.api.openApiObject.

In
`@packages/framework/http/libraries/open-api/src/openApi/services/v3Dot2/SwaggerUiProvider.spec.ts`:
- Around line 71-87: The test for the SwaggerUiProvider.openApiObject getter
must follow the four-layer describe convention (Class → Method → Input → Flow);
update the spec so the top-level describe names the class (SwaggerUiProvider),
the next describes the 'openApiObject' getter, add an Input-level describe for
the fixture/input context (e.g. 'when options contains api.openApiObject'
referencing optionsFixture), and change the Flow-level describe to a descriptive
condition (e.g. 'when called and the option is present') so the it() assertion
remains the same (expect(result).toBe(optionsFixture.api.openApiObject)); keep
references to SwaggerUiProvider, openApiObject, and optionsFixture to locate the
code.

In `@packages/framework/http/libraries/openapi-validation/package.json`:
- Around line 26-27: The package.json's devDependencies entry for ajv
("devDependencies.ajv") is pinned to 8.17.1 which does not satisfy the declared
peerDependencies.ajv range (^8.18.0); update devDependencies.ajv to a version
that matches the peer range (e.g., ^8.18.0 or a specific 8.18.x/8.19.x release)
so local tests run against a supported AJV version and the peer constraint is
honored.
- Around line 12-14: The package.json pins two internal packages to fixed
versions; change the dependencies for "@inversifyjs/prototype-utils" and
"@inversifyjs/reflect-metadata-utils" to use the workspace protocol
(workspace:*) like "@inversifyjs/validation-common" so they reference the
monorepo packages; update the dependency values for the exact keys
"@inversifyjs/prototype-utils" and "@inversifyjs/reflect-metadata-utils" to
"workspace:*".

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot1/pipes/OpenApiValidationPipe.int.spec.ts`:
- Around line 285-297: The test description in OpenApiValidationPipe.int.spec.ts
is incorrect: the describe currently says "no Content-Type header" but the test
sends a 'Content-Type': 'application/json' header and actually exercises the
case where the pipe has no requestContentTypeProvider and falls back to the
OpenAPI single declared content type; update the describe string to accurately
reflect this (e.g., mention "requestContentTypeProvider undefined / falls back
to single declared content type" or "with single content type and Content-Type
header present") so the description matches the behavior exercised by the test
that uses the POST /users request and the response variable.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot1/pipes/OpenApiValidationPipe.ts`:
- Around line 206-217: The content-type check in OpenApiValidationPipe (inside
the rawContentType !== undefined branch) compares the lowercased request media
type (baseContentType) against declaredContentTypes in their original casing
which can cause mismatches; normalize declaredContentTypes by mapping each entry
to a trimmed, lowercased form (e.g., normalizedDeclared =
declaredContentTypes.map(...) ) and use
normalizedDeclared.includes(baseContentType) for the comparison, then throw the
same InversifyValidationError (InversifyValidationErrorKind.validationFailed) if
not included and keep returning baseContentType.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.int.spec.ts`:
- Around line 58-91: The promise returned for bootstrapping the Server currently
only accepts a resolve callback and throws inside the httpServer.listen
callback, which won't reject the outer Promise and lacks an 'error' listener for
listen/start failures; update the Promise signature to accept both resolve and
reject, replace the throw when address is null/string with reject(new
Error(...)), attach an httpServer.once('error', reject) before calling
httpServer.listen and remove that listener (or use httpServer.off) once the
listen callback succeeds so startup errors properly reject the outer Promise,
and ensure any listeners are cleaned up on successful resolve.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.spec.ts`:
- Around line 30-49: The test claims "parameter metadata is undefined" but the
mock returns an empty array; change the mocked return of
getControllerMethodParameterMetadataList from [] to undefined
(vitest.mocked(getControllerMethodParameterMetadataList).mockReturnValueOnce(undefined))
so the OpenApiValidationPipe branch for undefined metadata is exercised; update
any related expectations/assertions in this spec (OpenApiValidationPipe.spec.ts)
to reflect the undefined-path behavior if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.ts`:
- Around line 206-217: OpenApiValidationPipe currently lowercases the
request-derived baseContentType but compares it against declaredContentTypes
which retain original casing, causing mismatches for differently cased media
types; update the check in the validation block (the code that computes
baseMediaType/baseContentType and uses declaredContentTypes.includes(...)) to
perform a case-insensitive comparison by normalizing declaredContentTypes (e.g.,
map to lowercase/trim) or by comparing baseContentType to each declared entry
lowercased, and keep the thrown InversifyValidationError behavior but use the
normalized values when matching.

---

Nitpick comments:
In
`@packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot1/getControllerOpenApiMetadata.spec.ts`:
- Around line 11-70: Refactor the spec for getControllerOpenApiMetadata to
follow the repository’s 4-layer describe hierarchy (Class → Method → Input →
Flow) and replace ad-hoc let fixtures with a reusable fixture class that exposes
static factory methods; specifically, wrap tests in nested describes for the
target class, the getControllerOpenApiMetadata method, the input case (metadata
present vs undefined), and the execution flow, and create a fixture class (e.g.,
ControllerFixture) with static methods to produce targetFixture and
metadataFixture; update test bodies to call
vitest.mocked(getOwnReflectMetadata).mockReturnValueOnce(...) and assert calls
and returns against controllerOpenApiMetadataReflectKey as before, but using the
new fixture class and describe structure to comply with the required pattern.

In
`@packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot1/getControllerOpenApiMetadata.ts`:
- Around line 6-13: Add JSDoc for the exported getControllerOpenApiMetadata
function: document its purpose (retrieves controller OpenAPI metadata from the
target via reflect metadata), describe the parameter `target: object` (the
controller/class whose metadata is read), mention the return type
`ControllerOpenApiMetadata | undefined` and what undefined means (no metadata
found), and reference the internal helpers/keys used (`getOwnReflectMetadata`,
`controllerOpenApiMetadataReflectKey`) for clarity; place the JSDoc immediately
above the getControllerOpenApiMetadata declaration and keep it concise and
consistent with other public API docs in the package.

In
`@packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot2/getControllerOpenApiMetadata.ts`:
- Around line 6-13: Add a JSDoc comment for the public function
getControllerOpenApiMetadata describing its purpose (retrieves controller
OpenAPI metadata via reflect metadata), document the parameter target (object)
and the return type ControllerOpenApiMetadata | undefined, and mention that it
uses getOwnReflectMetadata with controllerOpenApiMetadataReflectKey; follow the
style and wording used in the v3Dot1 counterpart to keep consistency across
versions.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.ts`:
- Around line 35-233: The two implementations of OpenApiValidationPipe (v3Dot1
and v3Dot2) duplicate logic; extract the common behavior into a shared base
class or utilities and have the version-specific classes only supply differences
(types, import SCHEMA_ID, and content-type mapping). Concretely, create a
BaseOpenApiValidationPipe that contains execute, `#getOrInitAjv`, and
`#resolveContentType` logic using generic type parameters or abstract getters for
the OpenAPI object type and SCHEMA_ID, then refactor the v3Dot2
OpenApiValidationPipe to extend BaseOpenApiValidationPipe and override/implement
only the version-specific pieces (e.g., providing this.#openApiObject type,
SCHEMA_ID constant, and any type aliases), doing the same for v3Dot1 so both
implementations reuse the shared logic.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 729742ee-fad4-4e73-a0e5-414c85d0733f

📥 Commits

Reviewing files that changed from the base of the PR and between ca6ede9 and 0fbd5ed.

📒 Files selected for processing (44)
  • .changeset/add-openapi-body-validation-http-core.md
  • .changeset/add-openapi-body-validation-http-open-api.md
  • .changeset/add-openapi-body-validation-new-package.md
  • .changeset/config.json
  • packages/docs/services/inversify-framework-site/blog/2026-04-04-openapi-body-validation/index.mdx
  • packages/docs/services/inversify-framework-site/validation-docs/openapi/_category_.json
  • packages/docs/services/inversify-framework-site/validation-docs/openapi/api.mdx
  • packages/docs/services/inversify-framework-site/validation-docs/openapi/introduction.mdx
  • packages/docs/tools/inversify-validation-code-examples/package.json
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiIntroduction.int.spec.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiIntroduction.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiOpenApiValidationPipe.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiValidateDecorator.ts
  • packages/framework/http/libraries/core/src/index.ts
  • packages/framework/http/libraries/open-api/src/index.ts
  • packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot1/getControllerOpenApiMetadata.spec.ts
  • packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot1/getControllerOpenApiMetadata.ts
  • packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot2/getControllerOpenApiMetadata.spec.ts
  • packages/framework/http/libraries/open-api/src/metadata/calculations/v3Dot2/getControllerOpenApiMetadata.ts
  • packages/framework/http/libraries/open-api/src/openApi/services/v3Dot1/SwaggerUiProvider.spec.ts
  • packages/framework/http/libraries/open-api/src/openApi/services/v3Dot1/SwaggerUiProvider.ts
  • packages/framework/http/libraries/open-api/src/openApi/services/v3Dot2/SwaggerUiProvider.spec.ts
  • packages/framework/http/libraries/open-api/src/openApi/services/v3Dot2/SwaggerUiProvider.ts
  • packages/framework/http/libraries/open-api/src/v3Dot2.ts
  • packages/framework/http/libraries/openapi-validation/.gitignore
  • packages/framework/http/libraries/openapi-validation/.lintstagedrc.json
  • packages/framework/http/libraries/openapi-validation/eslint.config.mjs
  • packages/framework/http/libraries/openapi-validation/package.json
  • packages/framework/http/libraries/openapi-validation/prettier.config.mjs
  • packages/framework/http/libraries/openapi-validation/src/common/decorators/Validate.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/common/decorators/Validate.ts
  • packages/framework/http/libraries/openapi-validation/src/common/reflectMetadata/openApiValidationMetadataReflectKey.ts
  • packages/framework/http/libraries/openapi-validation/src/index.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot1/pipes/OpenApiValidationPipe.int.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot1/pipes/OpenApiValidationPipe.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot1/pipes/OpenApiValidationPipe.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot2.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.int.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.ts
  • packages/framework/http/libraries/openapi-validation/stryker.config.mjs
  • packages/framework/http/libraries/openapi-validation/tsconfig.esm.json
  • packages/framework/http/libraries/openapi-validation/tsconfig.json
  • packages/framework/http/libraries/openapi-validation/vitest.config.mjs

Comment on lines +70 to +72
public get openApiObject(): OpenApi3Dot1Object {
return this.#options.api.openApiObject;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether openApiObject is consumed before provide() in call sites.
# Expected: no cases where swaggerProvider.openApiObject is passed into validators before swaggerProvider.provide(container).

rg -n -C6 --type=ts --type=tsx 'swaggerProvider\.openApiObject|swaggerProvider\.provide\('

Repository: inversify/monorepo

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Re-run with correct rg syntax - rg doesn't support 'tsx' as separate type
rg -n -C6 'swaggerProvider\.openApiObject|swaggerProvider\.provide\(' --type ts

Repository: inversify/monorepo

Length of output: 20225


🏁 Script executed:

#!/bin/bash
# Read the SwaggerUiProvider.ts file to understand the implementation
cat -n packages/framework/http/libraries/open-api/src/openApi/services/v3Dot1/SwaggerUiProvider.ts

Repository: inversify/monorepo

Length of output: 10932


🏁 Script executed:

#!/bin/bash
# Search for openApiObject getter and provide() method in the file
rg -n -A5 -B5 'openApiObject|provide\(' packages/framework/http/libraries/open-api/src/openApi/services/v3Dot1/SwaggerUiProvider.ts --type ts

Repository: inversify/monorepo

Length of output: 1006


Add a guard to prevent accessing the OpenAPI spec before initialization.

The openApiObject getter at line 70 lacks a check for the #provided state. While all current call sites properly invoke provide() before accessing the getter, adding a guard improves defensive programming and prevents misuse:

Proposed fix
 public get openApiObject(): OpenApi3Dot1Object {
+  if (!this.#provided) {
+    throw new Error('Cannot access openApiObject before provide()');
+  }
+
   return this.#options.api.openApiObject;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public get openApiObject(): OpenApi3Dot1Object {
return this.#options.api.openApiObject;
}
public get openApiObject(): OpenApi3Dot1Object {
if (!this.#provided) {
throw new Error('Cannot access openApiObject before provide()');
}
return this.#options.api.openApiObject;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/open-api/src/openApi/services/v3Dot1/SwaggerUiProvider.ts`
around lines 70 - 72, The openApiObject getter currently returns
this.#options.api.openApiObject without verifying initialization; add a guard
using the provider's internal flag (this.#provided) to throw a clear error if
openApiObject is accessed before provide() has been called. Update the
SwaggerUiProvider class's openApiObject getter to check this.#provided and throw
a descriptive Error (e.g., "OpenAPI object not provided — call provide() first")
when false, otherwise return this.#options.api.openApiObject.

Comment on lines +206 to +217
if (rawContentType !== undefined) {
const baseMediaType: string = rawContentType.split(';')[0] ?? '';
const baseContentType: string = baseMediaType.trim().toLowerCase();

if (!declaredContentTypes.includes(baseContentType)) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`Unsupported content type: ${baseContentType}. Supported: ${declaredContentTypes.join(', ')}`,
);
}

return baseContentType;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Content-type matching may fail due to case sensitivity.

Same issue as in the v3Dot2 implementation: the case-insensitive comparison between the request's content type (lowercased) and declared content types (original case) may fail for specs with mixed-case media types.

Proposed fix for case-insensitive comparison
     const rawContentType: string | undefined =
       this.#requestContentTypeProvider?.();
 
     if (rawContentType !== undefined) {
       const baseMediaType: string = rawContentType.split(';')[0] ?? '';
       const baseContentType: string = baseMediaType.trim().toLowerCase();
 
-      if (!declaredContentTypes.includes(baseContentType)) {
+      const matchedContentType: string | undefined = declaredContentTypes.find(
+        (ct: string): boolean => ct.toLowerCase() === baseContentType,
+      );
+
+      if (matchedContentType === undefined) {
         throw new InversifyValidationError(
           InversifyValidationErrorKind.validationFailed,
           `Unsupported content type: ${baseContentType}. Supported: ${declaredContentTypes.join(', ')}`,
         );
       }
 
-      return baseContentType;
+      return matchedContentType;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (rawContentType !== undefined) {
const baseMediaType: string = rawContentType.split(';')[0] ?? '';
const baseContentType: string = baseMediaType.trim().toLowerCase();
if (!declaredContentTypes.includes(baseContentType)) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`Unsupported content type: ${baseContentType}. Supported: ${declaredContentTypes.join(', ')}`,
);
}
return baseContentType;
if (rawContentType !== undefined) {
const baseMediaType: string = rawContentType.split(';')[0] ?? '';
const baseContentType: string = baseMediaType.trim().toLowerCase();
const matchedContentType: string | undefined = declaredContentTypes.find(
(ct: string): boolean => ct.toLowerCase() === baseContentType,
);
if (matchedContentType === undefined) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`Unsupported content type: ${baseContentType}. Supported: ${declaredContentTypes.join(', ')}`,
);
}
return matchedContentType;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot1/pipes/OpenApiValidationPipe.ts`
around lines 206 - 217, The content-type check in OpenApiValidationPipe (inside
the rawContentType !== undefined branch) compares the lowercased request media
type (baseContentType) against declaredContentTypes in their original casing
which can cause mismatches; normalize declaredContentTypes by mapping each entry
to a trimmed, lowercased form (e.g., normalizedDeclared =
declaredContentTypes.map(...) ) and use
normalizedDeclared.includes(baseContentType) for the comparison, then throw the
same InversifyValidationError (InversifyValidationErrorKind.validationFailed) if
not included and keep returning baseContentType.

Comment on lines +58 to +91
return new Promise<Server>(
(resolve: (value: Server | PromiseLike<Server>) => void) => {
httpServer.listen(0, '127.0.0.1', () => {
const address: AddressInfo | string | null = httpServer.address();

if (address === null || typeof address === 'string') {
throw new Error('Failed to get server address');
}

const server: Server = {
host: address.address,
port: address.port,
shutdown: async (): Promise<void> => {
await new Promise<void>(
(
resolve: (value: void | PromiseLike<void>) => void,
reject: (reason?: unknown) => void,
) => {
httpServer.close((error: Error | undefined) => {
if (error !== undefined) {
reject(error);
} else {
resolve();
}
});
},
);
},
};

resolve(server);
});
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Server bootstrap promise can hang or crash on startup failures

Line 63 throws inside an async listen callback, which does not safely reject the outer Promise, and there is no error listener for listen failures. This can cause flaky hangs/crashes in the integration suite.

Proposed fix
-  return new Promise<Server>(
-    (resolve: (value: Server | PromiseLike<Server>) => void) => {
-      httpServer.listen(0, '127.0.0.1', () => {
+  return new Promise<Server>(
+    (
+      resolve: (value: Server | PromiseLike<Server>) => void,
+      reject: (reason?: unknown) => void,
+    ) => {
+      const onError = (error: Error): void => {
+        reject(error);
+      };
+
+      httpServer.once('error', onError);
+      httpServer.listen(0, '127.0.0.1', () => {
+        httpServer.off('error', onError);
         const address: AddressInfo | string | null = httpServer.address();

         if (address === null || typeof address === 'string') {
-          throw new Error('Failed to get server address');
+          reject(new Error('Failed to get server address'));
+          return;
         }

         const server: Server = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.int.spec.ts`
around lines 58 - 91, The promise returned for bootstrapping the Server
currently only accepts a resolve callback and throws inside the
httpServer.listen callback, which won't reject the outer Promise and lacks an
'error' listener for listen/start failures; update the Promise signature to
accept both resolve and reject, replace the throw when address is null/string
with reject(new Error(...)), attach an httpServer.once('error', reject) before
calling httpServer.listen and remove that listener (or use httpServer.off) once
the listen callback succeeds so startup errors properly reject the outer
Promise, and ensure any listeners are cleaned up on successful resolve.

Comment on lines +30 to +49
describe('when called, and parameter metadata is undefined', () => {
let pipe: OpenApiValidationPipe;
let inputFixture: unknown;
let metadataFixture: PipeMetadata;
let result: unknown;

beforeAll(() => {
inputFixture = { name: 'test' };
metadataFixture = {
methodName: 'create',
parameterIndex: 0,
targetClass: class {} as NewableFunction,
};

pipe = new OpenApiValidationPipe({} as OpenApi3Dot2Object);

vitest
.mocked(getControllerMethodParameterMetadataList)
.mockReturnValueOnce([]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test case doesn’t exercise the branch its title describes

Line 30 says the metadata is undefined, but Line 48 returns []. This scenario currently validates the empty-list path, not the undefined path.

Proposed fix
-        vitest
-          .mocked(getControllerMethodParameterMetadataList)
-          .mockReturnValueOnce([]);
+        vitest
+          .mocked(getControllerMethodParameterMetadataList)
+          .mockReturnValueOnce(undefined);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('when called, and parameter metadata is undefined', () => {
let pipe: OpenApiValidationPipe;
let inputFixture: unknown;
let metadataFixture: PipeMetadata;
let result: unknown;
beforeAll(() => {
inputFixture = { name: 'test' };
metadataFixture = {
methodName: 'create',
parameterIndex: 0,
targetClass: class {} as NewableFunction,
};
pipe = new OpenApiValidationPipe({} as OpenApi3Dot2Object);
vitest
.mocked(getControllerMethodParameterMetadataList)
.mockReturnValueOnce([]);
describe('when called, and parameter metadata is undefined', () => {
let pipe: OpenApiValidationPipe;
let inputFixture: unknown;
let metadataFixture: PipeMetadata;
let result: unknown;
beforeAll(() => {
inputFixture = { name: 'test' };
metadataFixture = {
methodName: 'create',
parameterIndex: 0,
targetClass: class {} as NewableFunction,
};
pipe = new OpenApiValidationPipe({} as OpenApi3Dot2Object);
vitest
.mocked(getControllerMethodParameterMetadataList)
.mockReturnValueOnce(undefined);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.spec.ts`
around lines 30 - 49, The test claims "parameter metadata is undefined" but the
mock returns an empty array; change the mocked return of
getControllerMethodParameterMetadataList from [] to undefined
(vitest.mocked(getControllerMethodParameterMetadataList).mockReturnValueOnce(undefined))
so the OpenApiValidationPipe branch for undefined metadata is exercised; update
any related expectations/assertions in this spec (OpenApiValidationPipe.spec.ts)
to reflect the undefined-path behavior if needed.

Comment on lines +206 to +217
if (rawContentType !== undefined) {
const baseMediaType: string = rawContentType.split(';')[0] ?? '';
const baseContentType: string = baseMediaType.trim().toLowerCase();

if (!declaredContentTypes.includes(baseContentType)) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`Unsupported content type: ${baseContentType}. Supported: ${declaredContentTypes.join(', ')}`,
);
}

return baseContentType;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Content-type matching may fail due to case sensitivity.

The baseContentType is lowercased (line 208), but declaredContentTypes preserves the original casing from the OpenAPI spec. Per RFC 7231, media types are case-insensitive, so 'application/json' and 'Application/JSON' should match.

Proposed fix for case-insensitive comparison
     const rawContentType: string | undefined =
       this.#requestContentTypeProvider?.();
 
     if (rawContentType !== undefined) {
       const baseMediaType: string = rawContentType.split(';')[0] ?? '';
       const baseContentType: string = baseMediaType.trim().toLowerCase();
 
-      if (!declaredContentTypes.includes(baseContentType)) {
+      const matchedContentType: string | undefined = declaredContentTypes.find(
+        (ct: string): boolean => ct.toLowerCase() === baseContentType,
+      );
+
+      if (matchedContentType === undefined) {
         throw new InversifyValidationError(
           InversifyValidationErrorKind.validationFailed,
           `Unsupported content type: ${baseContentType}. Supported: ${declaredContentTypes.join(', ')}`,
         );
       }
 
-      return baseContentType;
+      return matchedContentType;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/v3Dot2/pipes/OpenApiValidationPipe.ts`
around lines 206 - 217, OpenApiValidationPipe currently lowercases the
request-derived baseContentType but compares it against declaredContentTypes
which retain original casing, causing mismatches for differently cased media
types; update the check in the validation block (the code that computes
baseMediaType/baseContentType and uses declaredContentTypes.includes(...)) to
perform a case-insensitive comparison by normalizing declaredContentTypes (e.g.,
map to lowercase/trim) or by comparing baseContentType to each declared entry
lowercased, and keep the thrown InversifyValidationError behavior but use the
normalized values when matching.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

Benchmarks results:

> root@ benchmarks:http:cjs:low /home/runner/work/monorepo/monorepo
> pnpm run --filter "@inversifyjs/http-benchmarks" benchmarks:run:cjs:low


> @inversifyjs/http-benchmarks@ benchmarks:run:cjs:low /home/runner/work/monorepo/monorepo/packages/framework/http/tools/http-benchmarks
> BUILD_TARGET=cjs VUS=128 ./bin/run-cjs.cjs

[CJS] Running benchmarks...

Express Basic Get Request
┌─────────┬───────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                 │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyExpress''6.999''6.506''9010.848'             │ 90217   │
│ 1       │ 'express''6.529''6.230''9686.443'             │ 96992   │
│ 2       │ 'NestJSExpress''7.188''7.020''8809.173'             │ 88213   │
└─────────┴───────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyExpress vs express Speedup: 0.930x
currentInversifyExpress vs NestJSExpress Speedup: 1.023x

Express v4 Basic Get Request
┌─────────┬────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                  │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyExpress4''7.137''6.825''8869.882'             │ 88822   │
│ 1       │ 'express4''6.834''6.474''9258.860'             │ 92699   │
└─────────┴────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyExpress4 vs express4 Speedup: 0.958x

Fastify Basic Get Request
┌─────────┬───────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                 │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyFastify''4.136''3.611''15111.427'            │ 151249  │
│ 1       │ 'fastify''3.774''3.315''16494.594'            │ 165114  │
│ 2       │ 'NestJSFastify''4.295''3.787''14469.561'            │ 144820  │
└─────────┴───────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyFastify vs fastify Speedup: 0.916x
currentInversifyFastify vs NestJSFastify Speedup: 1.044x

Hono Basic Get Request
┌─────────┬────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name              │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyHono''4.568''4.117''13701.084'            │ 137149  │
│ 1       │ 'hono''3.995''3.558''15610.128'            │ 156244  │
└─────────┴────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyHono vs hono Speedup: 0.878x

uWebSockets Basic Get Request
┌─────────┬───────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                     │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyUwebsockets''2.385''1.685''25352.125'            │ 253890  │
│ 1       │ 'uwebsockets''2.304''1.691''26556.561'            │ 265667  │
└─────────┴───────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyUwebsockets vs uwebsockets Speedup: 0.955x

A custom decorator is used instead to get the required data to validate request parts.

Validate decorator has been removed.

Validation pipes properly traverse openapi schemas instead of relying on metadata

getControllerOpenApiMetadata was never required
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

Benchmarks results:

> root@ benchmarks:http:cjs:low /home/runner/work/monorepo/monorepo
> pnpm run --filter "@inversifyjs/http-benchmarks" benchmarks:run:cjs:low


> @inversifyjs/http-benchmarks@ benchmarks:run:cjs:low /home/runner/work/monorepo/monorepo/packages/framework/http/tools/http-benchmarks
> BUILD_TARGET=cjs VUS=128 ./bin/run-cjs.cjs

[CJS] Running benchmarks...

Express Basic Get Request
┌─────────┬───────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                 │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyExpress''5.520''4.809''11426.820'            │ 114400  │
│ 1       │ 'express''5.114''4.809''12345.724'            │ 123606  │
│ 2       │ 'NestJSExpress''5.502''5.141''11485.383'            │ 114994  │
└─────────┴───────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyExpress vs express Speedup: 0.926x
currentInversifyExpress vs NestJSExpress Speedup: 0.995x

Express v4 Basic Get Request
┌─────────┬────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                  │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyExpress4''5.453''5.099''11592.843'            │ 116065  │
│ 1       │ 'express4''5.188''4.867''12172.195'            │ 121848  │
└─────────┴────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyExpress4 vs express4 Speedup: 0.952x

Fastify Basic Get Request
┌─────────┬───────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                 │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyFastify''2.959''2.405''20972.991'            │ 209801  │
│ 1       │ 'fastify''2.685''2.165''23025.793'            │ 230374  │
│ 2       │ 'NestJSFastify''3.057''2.574''20294.131'            │ 202991  │
└─────────┴───────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyFastify vs fastify Speedup: 0.911x
currentInversifyFastify vs NestJSFastify Speedup: 1.033x

Hono Basic Get Request
┌─────────┬────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name              │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyHono''3.291''2.772''18971.007'            │ 189849  │
│ 1       │ 'hono''2.935''2.391''21161.450'            │ 211733  │
└─────────┴────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyHono vs hono Speedup: 0.896x

uWebSockets Basic Get Request
┌─────────┬───────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                     │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyUwebsockets''2.041''1.445''29740.733'            │ 297460  │
│ 1       │ 'uwebsockets''1.980''1.437''30735.811'            │ 307413  │
└─────────┴───────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyUwebsockets vs uwebsockets Speedup: 0.968x

Copy link
Copy Markdown

@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: 8

♻️ Duplicate comments (1)
packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.int.spec.ts (1)

57-90: ⚠️ Potential issue | 🟠 Major

Handle server startup errors by rejecting the outer Promise.

Line 63 throws inside the listen callback, and there is no error listener before listen(). This can cause nondeterministic test hangs/crashes instead of a clean Promise rejection.

Proposed fix
-  return new Promise<Server>(
-    (resolve: (value: Server | PromiseLike<Server>) => void) => {
-      httpServer.listen(0, '127.0.0.1', () => {
+  return new Promise<Server>(
+    (
+      resolve: (value: Server | PromiseLike<Server>) => void,
+      reject: (reason?: unknown) => void,
+    ) => {
+      const onError = (error: Error): void => {
+        reject(error);
+      };
+
+      httpServer.once('error', onError);
+      httpServer.listen(0, '127.0.0.1', () => {
+        httpServer.off('error', onError);
         const address: AddressInfo | string | null = httpServer.address();

         if (address === null || typeof address === 'string') {
-          throw new Error('Failed to get server address');
+          reject(new Error('Failed to get server address'));
+          return;
         }
@@
-        resolve(server);
+        resolve(server);
       });
     },
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.int.spec.ts`
around lines 57 - 90, The test currently throws inside the httpServer.listen
callback and lacks an error listener, which can cause hangs; update the Promise
returned by createServer so it rejects on startup errors by attaching an
httpServer.on('error', handler) before calling httpServer.listen and calling
reject(error) from that handler, and replace the throw when address is
null/typeof string with reject(new Error(...)); ensure you remove the 'error'
listener after resolving or rejecting and keep the existing shutdown
implementation and resolve(server) behavior intact so resources are cleaned up.
🧹 Nitpick comments (4)
packages/framework/http/libraries/openapi-validation/src/validation/calculations/buildCompositeValidationHandler.spec.ts (1)

19-154: Align describe labels with the repo test naming convention.

The current scopes use having ... + nested when called. Please rename scoped describe labels to the when called, and [condition] pattern for consistency across *.spec.ts tests.

As per coding guidelines, test names in **/*.{spec,int.spec,spec-d}.ts should follow the 'when called, and [condition]' pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/buildCompositeValidationHandler.spec.ts`
around lines 19 - 154, Replace the outer describe labels that start with "having
..." with the repo's naming convention "when called, and [condition]" for each
scenario around buildCompositeValidationHandler: change "having non object input
params" → "when called, and given non-object input params"; "having null input
params" → "when called, and given null input params"; "having empty handler map
and object input params" → "when called, and given an empty handler map and
object input params"; and "having non handler map and object input params with
right discriminator" → "when called, and given a non-empty handler map and an
object input with the matching discriminator". Keep the inner test bodies and
the nested "when called" blocks consistent (remove or merge duplicate nested
"when called" labels if needed) and leave calls to
buildCompositeValidationHandler, ajvFixture, discriminatorHandlerPair, and
handlerMock unchanged.
packages/framework/http/libraries/openapi-validation/src/validation/calculations/getMimeType.spec.ts (1)

6-39: Use the standard when called, and [condition] scope labels in this spec.

The table cases are solid, but the describe.each title currently uses having %s. Please rename the scoped context labels to the repo’s standard condition format.

As per coding guidelines, test names in **/*.{spec,int.spec,spec-d}.ts should follow the 'when called, and [condition]' pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/getMimeType.spec.ts`
around lines 6 - 39, Rename the outer table-driven describe.each title from
'having %s' to the standard pattern 'when called, and %s' and remove the nested
describe('when called', ...) wrapper so the beforeAll and it blocks live
directly under the outer describe; keep the getMimeType calls and expectations
unchanged so each case reads "when called, and [condition]".
packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getRequestBodyObject.ts (1)

39-40: Minor typo: "derreferencedRequestBodyObject" → "dereferencedRequestBodyObject"

The variable name has a double 'r' typo. This is a minor readability issue.

🔤 Suggested fix
-  let derreferencedRequestBodyObject: JsonValueObject | undefined =
+  let dereferencedRequestBodyObject: JsonValueObject | undefined =
     requestBodyObject as unknown as JsonValueObject;

Also update references at lines 66, 68-70, and 73.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getRequestBodyObject.ts`
around lines 39 - 40, Rename the misspelled variable
derreferencedRequestBodyObject to dereferencedRequestBodyObject throughout
getRequestBodyObject (update its declaration and every usage/reference),
ensuring any subsequent assignments or checks that reference
derreferencedRequestBodyObject are updated to the new name so the function
continues to compile and behave the same.
packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getRequestBodyObject.ts (1)

39-40: Minor typo: "derreferencedRequestBodyObject" → "dereferencedRequestBodyObject"

Same typo as in the v3.1 implementation. Consider fixing in both files for consistency.

🔤 Suggested fix
-  let derreferencedRequestBodyObject: JsonValueObject | undefined =
+  let dereferencedRequestBodyObject: JsonValueObject | undefined =
     requestBodyObject as unknown as JsonValueObject;

Also update references at lines 66, 68-70, and 73.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getRequestBodyObject.ts`
around lines 39 - 40, Rename the misspelled variable
derreferencedRequestBodyObject to dereferencedRequestBodyObject in
getRequestBodyObject (and mirror the same rename in the v3.1 implementation),
updating all references including the declaration and usages at the noted
positions (lines referenced in the review: the initial declaration and
subsequent references around where it's used—e.g., the occurrences previously at
lines 66, 68-70, and 73) so the identifier is consistent across the file and
between versions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/getPath.spec.ts`:
- Around line 6-9: Add test tuples to the existing describe.each<[string,
string, string]> table (the array used in the 'having %s' test) that cover a
non-leading-slash path and an absolute URL; for example include cases like
['non-leading path', 'users', '/users'] and ['absolute url',
'http://example.com/users?name=John', '/users'] so the getPath extraction logic
(exercised by the 'having %s' test) is validated for paths without a leading
slash and for full absolute URLs with query strings.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/getPath.ts`:
- Around line 1-11: getPath currently slices absolute URLs and can return
host-prefixed values (e.g., "//host/users"); update getPath(url: string) to
normalize absolute URLs before extracting the path: detect absolute forms (start
with "http://" or "https://") and obtain the pathname (e.g., via the URL
constructor or by stripping scheme+host) then remove any query string; ensure
you still handle relative URLs and queries the same way so the function always
returns only the path portion (e.g., "/users").

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getOperationObject.ts`:
- Around line 23-31: Replace the broad Record<string, ...> cast and direct
indexing of pathItemObject with a safe key check: first validate that method is
one of the OpenAPI operation keys (e.g.,
'get','put','post','delete','options','head','patch','trace') and that (method
in pathItemObject) is true, then perform a typed index into pathItemObject
(e.g., (pathItemObject as OpenApi3Dot1PathItemObject)[method as keyof
OpenApi3Dot1PathItemObject]) to assign operationObject; if the key is missing
keep throwing the existing InversifyValidationError using
InversifyValidationErrorKind.validationFailed. Ensure you use the existing
variables pathItemObject, method, and operationObject in the guard to avoid the
unsafe Record<string, ...> cast.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/inferContentType.ts`:
- Around line 11-21: The current inferContentType logic can throw a runtime
TypeError when requestBodyObject.content is missing or not an object and also
gives an inaccurate message when there are zero content types; update the guard
in inferContentType to first validate that requestBodyObject.content is a plain
object and that Object.keys(requestBodyObject.content) yields at least one
entry, throw an InversifyValidationError (using
InversifyValidationErrorKind.validationFailed) with a clear message for the
zero-content case, and only return contentTypes[0] when contentTypes.length ===
1; reference requestBodyObject.content, contentTypes, inferContentType,
InversifyValidationError and InversifyValidationErrorKind when making the
change.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getOperationObject.spec.ts`:
- Around line 19-67: Add a unit test that covers uppercase HTTP methods:
duplicate the existing "having a pathItemObject with the method" scenario but
set methodFixture to 'GET' (uppercase) and assert
getOperationObject(openApiObjectFixture, methodFixture, pathFixture) still calls
getPathItemObject(openApiObjectFixture, pathFixture) and returns the same
operationObjectFixture; this ensures getOperationObject correctly normalizes or
handles uppercase methods when looking up pathItemObject entries.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getOperationObject.ts`:
- Around line 15-31: The lookup in getOperationObject uses the raw method and a
wide Record<string, OpenApi3Dot2OperationObject> cast which hides non-operation
keys and will miss uppercase methods; normalize the incoming method to lowercase
(e.g., const methodKey = method.toLowerCase()) and use a typed key lookup
against OpenApi3Dot2PathItemObject (cast to keyof OpenApi3Dot2PathItemObject)
instead of Record<string,...> so you only access known operation properties;
keep the existing InversifyValidationError logic when the operation is
undefined.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/inferContentType.ts`:
- Around line 11-21: The inferContentType logic should guard for a
missing/nullable requestBodyObject.content and handle zero vs multiple entries
distinctly: first normalize contentTypes = requestBodyObject.content ?
Object.keys(requestBodyObject.content) : []; then if contentTypes.length === 1
return that value; if length === 0 throw an InversifyValidationError
(InversifyValidationErrorKind.validationFailed) with a message stating no
content type provided for method ${method}; otherwise (length > 1) throw the
existing multi-content error including contentTypes.join(', ') to preserve
current behavior.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.int.spec.ts`:
- Around line 279-290: The test block "when a valid POST /users request is made
with single content type and no Content-Type header" is sending a Content-Type
header in the beforeAll fetch call, so change the fetch invocation inside that
describe/beforeAll to omit the 'Content-Type' header (remove headers or pass an
empty headers object) so the fallback/content-type inference branch is actually
exercised; ensure you update the fetch call in this spec (the beforeAll that
sets response) and keep the body as JSON stringified as-is.

---

Duplicate comments:
In
`@packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.int.spec.ts`:
- Around line 57-90: The test currently throws inside the httpServer.listen
callback and lacks an error listener, which can cause hangs; update the Promise
returned by createServer so it rejects on startup errors by attaching an
httpServer.on('error', handler) before calling httpServer.listen and calling
reject(error) from that handler, and replace the throw when address is
null/typeof string with reject(new Error(...)); ensure you remove the 'error'
listener after resolving or rejecting and keep the existing shutdown
implementation and resolve(server) behavior intact so resources are cleaned up.

---

Nitpick comments:
In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/buildCompositeValidationHandler.spec.ts`:
- Around line 19-154: Replace the outer describe labels that start with "having
..." with the repo's naming convention "when called, and [condition]" for each
scenario around buildCompositeValidationHandler: change "having non object input
params" → "when called, and given non-object input params"; "having null input
params" → "when called, and given null input params"; "having empty handler map
and object input params" → "when called, and given an empty handler map and
object input params"; and "having non handler map and object input params with
right discriminator" → "when called, and given a non-empty handler map and an
object input with the matching discriminator". Keep the inner test bodies and
the nested "when called" blocks consistent (remove or merge duplicate nested
"when called" labels if needed) and leave calls to
buildCompositeValidationHandler, ajvFixture, discriminatorHandlerPair, and
handlerMock unchanged.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/getMimeType.spec.ts`:
- Around line 6-39: Rename the outer table-driven describe.each title from
'having %s' to the standard pattern 'when called, and %s' and remove the nested
describe('when called', ...) wrapper so the beforeAll and it blocks live
directly under the outer describe; keep the getMimeType calls and expectations
unchanged so each case reads "when called, and [condition]".

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getRequestBodyObject.ts`:
- Around line 39-40: Rename the misspelled variable
derreferencedRequestBodyObject to dereferencedRequestBodyObject throughout
getRequestBodyObject (update its declaration and every usage/reference),
ensuring any subsequent assignments or checks that reference
derreferencedRequestBodyObject are updated to the new name so the function
continues to compile and behave the same.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getRequestBodyObject.ts`:
- Around line 39-40: Rename the misspelled variable
derreferencedRequestBodyObject to dereferencedRequestBodyObject in
getRequestBodyObject (and mirror the same rename in the v3.1 implementation),
updating all references including the declaration and usages at the noted
positions (lines referenced in the review: the initial declaration and
subsequent references around where it's used—e.g., the occurrences previously at
lines 66, 68-70, and 73) so the identifier is consistent across the file and
between versions.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c819af5-8694-4c82-9410-ca5d20dffad8

📥 Commits

Reviewing files that changed from the base of the PR and between d5e1698 and 6ab73fb.

📒 Files selected for processing (53)
  • .changeset/add-openapi-body-validation-new-package.md
  • packages/docs/tools/inversify-validation-code-examples/package.json
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiIntroduction.int.spec.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiIntroduction.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiOpenApiValidationPipe.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiValidateDecorator.ts
  • packages/framework/http/libraries/open-api/src/index.ts
  • packages/framework/http/libraries/open-api/src/v3Dot2.ts
  • packages/framework/http/libraries/openapi-validation/package.json
  • packages/framework/http/libraries/openapi-validation/src/index.ts
  • packages/framework/http/libraries/openapi-validation/src/metadata/actions/setValidateMetadata.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/metadata/actions/setValidateMetadata.ts
  • packages/framework/http/libraries/openapi-validation/src/metadata/decorators/ValidatedBody.ts
  • packages/framework/http/libraries/openapi-validation/src/metadata/models/openApiValidationMetadataReflectKey.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot1.ts
  • packages/framework/http/libraries/openapi-validation/src/v3Dot2.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/buildCompositeValidationHandler.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/buildCompositeValidationHandler.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/getMimeType.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/getMimeType.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/getPath.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/getPath.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getOperationObject.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getOperationObject.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getPathItemObject.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getPathItemObject.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getRequestBodyObject.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getRequestBodyObject.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/handleBodyValidation.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/handleBodyValidation.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/inferContentType.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/inferContentType.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getOperationObject.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getOperationObject.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getPathItemObject.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getPathItemObject.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getRequestBodyObject.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getRequestBodyObject.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/handleBodyValidation.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/handleBodyValidation.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/inferContentType.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/inferContentType.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/BodyValidationInputParam.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/DiscriminatorValidationHandlerPair.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/ValidatedDecoratorResult.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/ValidationHandler.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/v3Dot1/schemaId.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/v3Dot2/schemaId.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/validatedInputParamTypes.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot1/OpenApiValidationPipe.int.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot1/OpenApiValidationPipe.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.int.spec.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.ts
✅ Files skipped from review due to trivial changes (9)
  • packages/docs/tools/inversify-validation-code-examples/package.json
  • packages/framework/http/libraries/openapi-validation/src/v3Dot1.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/ValidatedDecoratorResult.ts
  • .changeset/add-openapi-body-validation-new-package.md
  • packages/framework/http/libraries/openapi-validation/src/validation/models/v3Dot1/schemaId.ts
  • packages/framework/http/libraries/open-api/src/index.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/BodyValidationInputParam.ts
  • packages/framework/http/libraries/openapi-validation/src/validation/models/DiscriminatorValidationHandlerPair.ts
  • packages/framework/http/libraries/openapi-validation/package.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/framework/http/libraries/openapi-validation/src/index.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiValidateDecorator.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiIntroduction.int.spec.ts
  • packages/docs/tools/inversify-validation-code-examples/src/examples/v1/openApiValidation/apiOpenApiValidationPipe.ts

Comment on lines +6 to +9
describe.each<[string, string, string]>([
['url with no query', '/users', '/users'],
['url with query', '/users?name=John', '/users'],
])('having %s', (_: string, url: string, expected: string) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add cases for non-leading and absolute URLs.

Current cases don't cover the non-leading-slash path branch, so incorrect extraction can slip through.

🧪 Suggested additions
   describe.each<[string, string, string]>([
     ['url with no query', '/users', '/users'],
     ['url with query', '/users?name=John', '/users'],
+    ['url without leading slash', 'users?name=John', '/users'],
+    ['absolute url with query', 'https://example.com/users?name=John', '/users'],
   ])('having %s', (_: string, url: string, expected: string) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe.each<[string, string, string]>([
['url with no query', '/users', '/users'],
['url with query', '/users?name=John', '/users'],
])('having %s', (_: string, url: string, expected: string) => {
describe.each<[string, string, string]>([
['url with no query', '/users', '/users'],
['url with query', '/users?name=John', '/users'],
['url without leading slash', 'users?name=John', '/users'],
['absolute url with query', 'https://example.com/users?name=John', '/users'],
])('having %s', (_: string, url: string, expected: string) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/getPath.spec.ts`
around lines 6 - 9, Add test tuples to the existing describe.each<[string,
string, string]> table (the array used in the 'having %s' test) that cover a
non-leading-slash path and an absolute URL; for example include cases like
['non-leading path', 'users', '/users'] and ['absolute url',
'http://example.com/users?name=John', '/users'] so the getPath extraction logic
(exercised by the 'having %s' test) is validated for paths without a leading
slash and for full absolute URLs with query strings.

Comment on lines +1 to +11
export function getPath(url: string): string {
const queryIndex: number = url.indexOf('?');

if (url.startsWith('/')) {
return queryIndex === -1 ? url : url.substring(0, queryIndex);
}

return queryIndex === -1
? url.substring(url.indexOf('/'))
: url.substring(url.indexOf('/'), queryIndex);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Path extraction is wrong for absolute URLs.

If url is absolute, current slicing can produce host-prefixed values (e.g., //host/users) that won't match OpenAPI paths keys.

🔧 Proposed fix
 export function getPath(url: string): string {
-  const queryIndex: number = url.indexOf('?');
-
-  if (url.startsWith('/')) {
-    return queryIndex === -1 ? url : url.substring(0, queryIndex);
-  }
-
-  return queryIndex === -1
-    ? url.substring(url.indexOf('/'))
-    : url.substring(url.indexOf('/'), queryIndex);
+  return new URL(url, 'http://localhost').pathname;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getPath(url: string): string {
const queryIndex: number = url.indexOf('?');
if (url.startsWith('/')) {
return queryIndex === -1 ? url : url.substring(0, queryIndex);
}
return queryIndex === -1
? url.substring(url.indexOf('/'))
: url.substring(url.indexOf('/'), queryIndex);
}
export function getPath(url: string): string {
return new URL(url, 'http://localhost').pathname;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/getPath.ts`
around lines 1 - 11, getPath currently slices absolute URLs and can return
host-prefixed values (e.g., "//host/users"); update getPath(url: string) to
normalize absolute URLs before extracting the path: detect absolute forms (start
with "http://" or "https://") and obtain the pathname (e.g., via the URL
constructor or by stripping scheme+host) then remove any query string; ensure
you still handle relative URLs and queries the same way so the function always
returns only the path portion (e.g., "/users").

Comment on lines +23 to +31
const operationObject: OpenApi3Dot1OperationObject | undefined = (
pathItemObject as Record<string, OpenApi3Dot1OperationObject>
)[method];

if (operationObject === undefined) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`No OpenAPI operation found for method ${method} for path ${path}`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid broad Record<string, ...> casting for operation lookup.

The current cast allows arbitrary/prototype keys to be treated as operations, which can return non-operation values and shift failures to later code paths with misleading errors. Validate method keys first, then index with constrained keys.

💡 Suggested fix
 export function getOperationObject(
   openApiObject: OpenApi3Dot1Object,
   method: string,
   path: string,
 ): OpenApi3Dot1OperationObject {
@@
-  const operationObject: OpenApi3Dot1OperationObject | undefined = (
-    pathItemObject as Record<string, OpenApi3Dot1OperationObject>
-  )[method];
+  const allowedMethods: ReadonlySet<string> = new Set([
+    'delete',
+    'get',
+    'head',
+    'options',
+    'patch',
+    'post',
+    'put',
+    'trace',
+  ]);
+
+  if (!allowedMethods.has(method)) {
+    throw new InversifyValidationError(
+      InversifyValidationErrorKind.validationFailed,
+      `No OpenAPI operation found for method ${method} for path ${path}`,
+    );
+  }
+
+  const operationObject: OpenApi3Dot1OperationObject | undefined =
+    pathItemObject[method as keyof OpenApi3Dot1PathItemObject] as
+      | OpenApi3Dot1OperationObject
+      | undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const operationObject: OpenApi3Dot1OperationObject | undefined = (
pathItemObject as Record<string, OpenApi3Dot1OperationObject>
)[method];
if (operationObject === undefined) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`No OpenAPI operation found for method ${method} for path ${path}`,
);
export function getOperationObject(
openApiObject: OpenApi3Dot1Object,
method: string,
path: string,
): OpenApi3Dot1OperationObject {
const allowedMethods: ReadonlySet<string> = new Set([
'delete',
'get',
'head',
'options',
'patch',
'post',
'put',
]);
if (!allowedMethods.has(method)) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`No OpenAPI operation found for method ${method} for path ${path}`,
);
}
const operationObject: OpenApi3Dot1OperationObject | undefined =
pathItemObject[method as keyof OpenApi3Dot1PathItemObject] as
| OpenApi3Dot1OperationObject
| undefined;
if (operationObject === undefined) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`No OpenAPI operation found for method ${method} for path ${path}`,
);
}
return operationObject;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/getOperationObject.ts`
around lines 23 - 31, Replace the broad Record<string, ...> cast and direct
indexing of pathItemObject with a safe key check: first validate that method is
one of the OpenAPI operation keys (e.g.,
'get','put','post','delete','options','head','patch','trace') and that (method
in pathItemObject) is true, then perform a typed index into pathItemObject
(e.g., (pathItemObject as OpenApi3Dot1PathItemObject)[method as keyof
OpenApi3Dot1PathItemObject]) to assign operationObject; if the key is missing
keep throwing the existing InversifyValidationError using
InversifyValidationErrorKind.validationFailed. Ensure you use the existing
variables pathItemObject, method, and operationObject in the guard to avoid the
unsafe Record<string, ...> cast.

Comment on lines +11 to +21
const contentTypes: string[] = Object.keys(requestBodyObject.content);

if (contentTypes.length === 1) {
return contentTypes[0] as string;
}

throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`Cannot determine content type for request body validation for method ${method}: no content type provided and multiple content types defined in OpenAPI spec (${contentTypes.join(
', ',
)})`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mirror the same guard/error-text fix here to avoid runtime TypeError.

As written, malformed requestBodyObject.content can bypass domain error handling, and the message is inaccurate for the zero-content case.

💡 Suggested fix
 export function inferContentType(
   requestBodyObject: OpenApi3Dot1RequestBodyObject,
   method: string,
 ): string {
-  const contentTypes: string[] = Object.keys(requestBodyObject.content);
+  const contentTypes: string[] = Object.keys(requestBodyObject.content ?? {});
 
   if (contentTypes.length === 1) {
-    return contentTypes[0] as string;
+    return contentTypes[0];
   }
+
+  const contentTypesErrorReason: string =
+    contentTypes.length === 0
+      ? 'no content types defined in OpenAPI spec'
+      : `multiple content types defined in OpenAPI spec (${contentTypes.join(', ')})`;
 
   throw new InversifyValidationError(
     InversifyValidationErrorKind.validationFailed,
-    `Cannot determine content type for request body validation for method ${method}: no content type provided and multiple content types defined in OpenAPI spec (${contentTypes.join(
-      ', ',
-    )})`,
+    `Cannot determine content type for request body validation for method ${method}: no content type provided and ${contentTypesErrorReason}`,
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const contentTypes: string[] = Object.keys(requestBodyObject.content);
if (contentTypes.length === 1) {
return contentTypes[0] as string;
}
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`Cannot determine content type for request body validation for method ${method}: no content type provided and multiple content types defined in OpenAPI spec (${contentTypes.join(
', ',
)})`,
const contentTypes: string[] = Object.keys(requestBodyObject.content ?? {});
if (contentTypes.length === 1) {
return contentTypes[0];
}
const contentTypesErrorReason: string =
contentTypes.length === 0
? 'no content types defined in OpenAPI spec'
: `multiple content types defined in OpenAPI spec (${contentTypes.join(', ')})`;
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`Cannot determine content type for request body validation for method ${method}: no content type provided and ${contentTypesErrorReason}`,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot1/inferContentType.ts`
around lines 11 - 21, The current inferContentType logic can throw a runtime
TypeError when requestBodyObject.content is missing or not an object and also
gives an inaccurate message when there are zero content types; update the guard
in inferContentType to first validate that requestBodyObject.content is a plain
object and that Object.keys(requestBodyObject.content) yields at least one
entry, throw an InversifyValidationError (using
InversifyValidationErrorKind.validationFailed) with a clear message for the
zero-content case, and only return contentTypes[0] when contentTypes.length ===
1; reference requestBodyObject.content, contentTypes, inferContentType,
InversifyValidationError and InversifyValidationErrorKind when making the
change.

Comment on lines +19 to +67
describe('having a pathItemObject with the method', () => {
let openApiObjectFixture: OpenApi3Dot2Object;
let methodFixture: string;
let pathFixture: string;
let operationObjectFixture: OpenApi3Dot2OperationObject;
let pathItemObjectFixture: OpenApi3Dot2PathItemObject;

beforeAll(() => {
methodFixture = 'get';
pathFixture = '/users';
operationObjectFixture = {
responses: {},
};
pathItemObjectFixture = {
[methodFixture]: operationObjectFixture,
};
openApiObjectFixture = Symbol() as unknown as OpenApi3Dot2Object;
});

describe('when called', () => {
let result: unknown;

beforeAll(() => {
vitest
.mocked(getPathItemObject)
.mockReturnValueOnce(pathItemObjectFixture);

result = getOperationObject(
openApiObjectFixture,
methodFixture,
pathFixture,
);
});

afterAll(() => {
vitest.clearAllMocks();
});

it('should call getPathItemObject()', () => {
expect(getPathItemObject).toHaveBeenCalledExactlyOnceWith(
openApiObjectFixture,
pathFixture,
);
});

it('should return expected result', () => {
expect(result).toBe(operationObjectFixture);
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a case for uppercase HTTP methods.

Given runtime methods are often uppercase, add a GET input case to lock expected lookup behavior and prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getOperationObject.spec.ts`
around lines 19 - 67, Add a unit test that covers uppercase HTTP methods:
duplicate the existing "having a pathItemObject with the method" scenario but
set methodFixture to 'GET' (uppercase) and assert
getOperationObject(openApiObjectFixture, methodFixture, pathFixture) still calls
getPathItemObject(openApiObjectFixture, pathFixture) and returns the same
operationObjectFixture; this ensures getOperationObject correctly normalizes or
handles uppercase methods when looking up pathItemObject entries.

Comment on lines +15 to +31
method: string,
path: string,
): OpenApi3Dot2OperationObject {
const pathItemObject: OpenApi3Dot2PathItemObject = getPathItemObject(
openApiObject,
path,
);

const operationObject: OpenApi3Dot2OperationObject | undefined = (
pathItemObject as Record<string, OpenApi3Dot2OperationObject>
)[method];

if (operationObject === undefined) {
throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`No OpenAPI operation found for method ${method} for path ${path}`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize method before lookup and avoid wide string-key cast.

Using raw method can miss valid operations for uppercase inputs (e.g., GET), and the Record<string, ...> cast hides non-operation keys.

🔧 Proposed fix
 export function getOperationObject(
   openApiObject: OpenApi3Dot2Object,
   method: string,
   path: string,
 ): OpenApi3Dot2OperationObject {
@@
-  const operationObject: OpenApi3Dot2OperationObject | undefined = (
-    pathItemObject as Record<string, OpenApi3Dot2OperationObject>
-  )[method];
+  type PathItemMethodKey =
+    | 'delete'
+    | 'get'
+    | 'head'
+    | 'options'
+    | 'patch'
+    | 'post'
+    | 'put'
+    | 'query'
+    | 'trace';
+
+  const normalizedMethod: PathItemMethodKey = method.toLowerCase() as PathItemMethodKey;
+  const operationObject: OpenApi3Dot2OperationObject | undefined =
+    pathItemObject[normalizedMethod];
@@
-      `No OpenAPI operation found for method ${method} for path ${path}`,
+      `No OpenAPI operation found for method ${method} for path ${path}`,
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/getOperationObject.ts`
around lines 15 - 31, The lookup in getOperationObject uses the raw method and a
wide Record<string, OpenApi3Dot2OperationObject> cast which hides non-operation
keys and will miss uppercase methods; normalize the incoming method to lowercase
(e.g., const methodKey = method.toLowerCase()) and use a typed key lookup
against OpenApi3Dot2PathItemObject (cast to keyof OpenApi3Dot2PathItemObject)
instead of Record<string,...> so you only access known operation properties;
keep the existing InversifyValidationError logic when the operation is
undefined.

Comment on lines +11 to +21
const contentTypes: string[] = Object.keys(requestBodyObject.content);

if (contentTypes.length === 1) {
return contentTypes[0] as string;
}

throw new InversifyValidationError(
InversifyValidationErrorKind.validationFailed,
`Cannot determine content type for request body validation for method ${method}: no content type provided and multiple content types defined in OpenAPI spec (${contentTypes.join(
', ',
)})`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle missing/empty content explicitly before key extraction.

This path can currently throw a native TypeError when content is absent/null, and the thrown validation message is misleading when there are zero content types (it always says “multiple”). Please normalize and branch by count before throwing.

💡 Suggested fix
 export function inferContentType(
   requestBodyObject: OpenApi3Dot2RequestBodyObject,
   method: string,
 ): string {
-  const contentTypes: string[] = Object.keys(requestBodyObject.content);
+  const contentTypes: string[] = Object.keys(requestBodyObject.content ?? {});
 
   if (contentTypes.length === 1) {
-    return contentTypes[0] as string;
+    return contentTypes[0];
   }
+
+  const contentTypesErrorReason: string =
+    contentTypes.length === 0
+      ? 'no content types defined in OpenAPI spec'
+      : `multiple content types defined in OpenAPI spec (${contentTypes.join(', ')})`;
 
   throw new InversifyValidationError(
     InversifyValidationErrorKind.validationFailed,
-    `Cannot determine content type for request body validation for method ${method}: no content type provided and multiple content types defined in OpenAPI spec (${contentTypes.join(
-      ', ',
-    )})`,
+    `Cannot determine content type for request body validation for method ${method}: no content type provided and ${contentTypesErrorReason}`,
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/calculations/v3Dot2/inferContentType.ts`
around lines 11 - 21, The inferContentType logic should guard for a
missing/nullable requestBodyObject.content and handle zero vs multiple entries
distinctly: first normalize contentTypes = requestBodyObject.content ?
Object.keys(requestBodyObject.content) : []; then if contentTypes.length === 1
return that value; if length === 0 throw an InversifyValidationError
(InversifyValidationErrorKind.validationFailed) with a message stating no
content type provided for method ${method}; otherwise (length > 1) throw the
existing multi-content error including contentTypes.join(', ') to preserve
current behavior.

Comment on lines +279 to +290
describe('when a valid POST /users request is made with single content type and no Content-Type header', () => {
let response: Response;

beforeAll(async () => {
response = await fetch(
`http://${server.host}:${server.port.toString()}/users`,
{
body: JSON.stringify({ name: 'Alice' }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The fallback scenario currently sends a Content-Type header, so the branch is not actually tested.

Line 287 sets Content-Type: application/json while the scenario claims no header. This bypasses the intended content-type inference path.

Proposed fix
     describe('when a valid POST /users request is made with single content type and no Content-Type header', () => {
@@
       beforeAll(async () => {
         response = await fetch(
           `http://${server.host}:${server.port.toString()}/users`,
           {
-            body: JSON.stringify({ name: 'Alice' }),
-            headers: { 'Content-Type': 'application/json' },
+            body: new TextEncoder().encode(JSON.stringify({ name: 'Alice' })),
             method: 'POST',
           },
         );
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('when a valid POST /users request is made with single content type and no Content-Type header', () => {
let response: Response;
beforeAll(async () => {
response = await fetch(
`http://${server.host}:${server.port.toString()}/users`,
{
body: JSON.stringify({ name: 'Alice' }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
},
);
describe('when a valid POST /users request is made with single content type and no Content-Type header', () => {
let response: Response;
beforeAll(async () => {
response = await fetch(
`http://${server.host}:${server.port.toString()}/users`,
{
body: new TextEncoder().encode(JSON.stringify({ name: 'Alice' })),
method: 'POST',
},
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/validation/pipes/v3Dot2/OpenApiValidationPipe.int.spec.ts`
around lines 279 - 290, The test block "when a valid POST /users request is made
with single content type and no Content-Type header" is sending a Content-Type
header in the beforeAll fetch call, so change the fetch invocation inside that
describe/beforeAll to omit the 'Content-Type' header (remove headers or pass an
empty headers object) so the fallback/content-type inference branch is actually
exercised; ensure you update the fetch call in this spec (the beforeAll that
sets response) and keep the body as JSON stringified as-is.

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
openspec/changes/add-openapi-body-validation/proposal.md (1)

13-13: Add an explicit migration subsection for the @Validate() + @Body() replacement.

Line 13 states the old approach is replaced, but the proposal should include a short “before/after” migration note and compatibility expectation (supported/deprecated/removed) to reduce ambiguity for adopters.

Based on learnings: Maintain API compatibility across versions in inversifyjs/common - a foundation package whose changes affect the entire ecosystem.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/add-openapi-body-validation/proposal.md` at line 13, The
proposal currently states that the original `@Validate()` + `@Body()` approach
is replaced but lacks a migration note; add a concise "Migration" subsection to
the proposal that shows a before/after example converting `@Validate()` +
`@Body()` usage to the new pipe-based approach, state the compatibility
expectation (e.g., supported, deprecated, or removed) for `@Validate()`,
`@Body()`, and `PipeMetadata`, and mention any necessary steps (e.g., updating
to the new pipe that requires request context) so adopters know how to migrate;
reference the symbols `@Validate()`, `@Body()`, and `PipeMetadata` in that
subsection.
packages/framework/http/libraries/openapi-validation/src/metadata/decorators/ValidatedBody.spec.ts (1)

110-355: Extract repeated handler setup into reusable fixture builders.

The per-scenario setup is heavily duplicated (request/response/body/method/url/options/expected object). Please move this into fixture classes with static helpers to reduce drift and simplify future behavior changes.

♻️ Refactor sketch
+class ValidatedBodyHandlerFixture {
+  public static buildOptions(
+    overrides: Partial<CustomParameterDecoratorHandlerOptions<unknown, unknown>>,
+  ): CustomParameterDecoratorHandlerOptions<unknown, unknown> {
+    return {
+      getBody: vitest.fn(),
+      getHeaders: vitest.fn(),
+      getMethod: vitest.fn(),
+      getUrl: vitest.fn(),
+      ...overrides,
+    } as unknown as CustomParameterDecoratorHandlerOptions<unknown, unknown>;
+  }
+
+  public static buildExpected(params: {
+    body: unknown;
+    contentType: string | undefined;
+    method: string;
+    url: string;
+  }): BodyValidationInputParam<unknown> {
+    return {
+      body: params.body,
+      contentType: params.contentType,
+      method: params.method,
+      type: validatedInputParamBodyType,
+      url: params.url,
+    };
+  }
+}

As per coding guidelines **/*.{spec,int.spec}.ts: Create reusable test fixtures as classes with static methods following fixture guidelines from docs/testing/fixtures.md.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/framework/http/libraries/openapi-validation/src/metadata/decorators/ValidatedBody.spec.ts`
around lines 110 - 355, The tests repeat identical setup for
requestFixture/responseFixture/bodyFixture/methodFixture/urlFixture/options and
expected BodyValidationInputParam across many describe blocks; refactor by
creating a TestFixture class (e.g., BodyHandlerTestFixture) with static builders
that produce the options object (matching
CustomParameterDecoratorHandlerOptions) and expected result, and helper methods
to call handler and return result; update each describe to call the fixtures
instead of duplicating setup and keep references to handler, getMimeType,
BodyValidationInputParam, and validatedInputParamBodyType so behavior and
assertions remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openspec/changes/add-openapi-body-validation/proposal.md`:
- Line 13: The proposal currently states that the original `@Validate()` +
`@Body()` approach is replaced but lacks a migration note; add a concise
"Migration" subsection to the proposal that shows a before/after example
converting `@Validate()` + `@Body()` usage to the new pipe-based approach, state
the compatibility expectation (e.g., supported, deprecated, or removed) for
`@Validate()`, `@Body()`, and `PipeMetadata`, and mention any necessary steps
(e.g., updating to the new pipe that requires request context) so adopters know
how to migrate; reference the symbols `@Validate()`, `@Body()`, and
`PipeMetadata` in that subsection.

In
`@packages/framework/http/libraries/openapi-validation/src/metadata/decorators/ValidatedBody.spec.ts`:
- Around line 110-355: The tests repeat identical setup for
requestFixture/responseFixture/bodyFixture/methodFixture/urlFixture/options and
expected BodyValidationInputParam across many describe blocks; refactor by
creating a TestFixture class (e.g., BodyHandlerTestFixture) with static builders
that produce the options object (matching
CustomParameterDecoratorHandlerOptions) and expected result, and helper methods
to call handler and return result; update each describe to call the fixtures
instead of duplicating setup and keep references to handler, getMimeType,
BodyValidationInputParam, and validatedInputParamBodyType so behavior and
assertions remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e0d7a25-cbb3-4f04-bd16-65cb2aeb1f8c

📥 Commits

Reviewing files that changed from the base of the PR and between 6ab73fb and 4e35f90.

📒 Files selected for processing (8)
  • openspec/changes/add-openapi-body-validation/design.md
  • openspec/changes/add-openapi-body-validation/proposal.md
  • openspec/changes/add-openapi-body-validation/specs/openapi-body-validation/spec.md
  • openspec/changes/add-openapi-body-validation/tasks.md
  • packages/docs/services/inversify-framework-site/blog/2026-04-04-openapi-body-validation/index.mdx
  • packages/docs/services/inversify-framework-site/validation-docs/openapi/api.mdx
  • packages/docs/services/inversify-framework-site/validation-docs/openapi/introduction.mdx
  • packages/framework/http/libraries/openapi-validation/src/metadata/decorators/ValidatedBody.spec.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/docs/services/inversify-framework-site/validation-docs/openapi/introduction.mdx
  • packages/docs/services/inversify-framework-site/validation-docs/openapi/api.mdx
  • openspec/changes/add-openapi-body-validation/design.md

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

Benchmarks results:

> root@ benchmarks:http:cjs:low /home/runner/work/monorepo/monorepo
> pnpm run --filter "@inversifyjs/http-benchmarks" benchmarks:run:cjs:low


> @inversifyjs/http-benchmarks@ benchmarks:run:cjs:low /home/runner/work/monorepo/monorepo/packages/framework/http/tools/http-benchmarks
> BUILD_TARGET=cjs VUS=128 ./bin/run-cjs.cjs

[CJS] Running benchmarks...

Express Basic Get Request
┌─────────┬───────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                 │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyExpress''5.509''4.856''11439.922'            │ 114576  │
│ 1       │ 'express''5.100''4.662''12382.908'            │ 123904  │
│ 2       │ 'NestJSExpress''5.527''5.126''11428.177'            │ 114407  │
└─────────┴───────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyExpress vs express Speedup: 0.924x
currentInversifyExpress vs NestJSExpress Speedup: 1.001x

Express v4 Basic Get Request
┌─────────┬────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                  │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyExpress4''5.444''5.136''11613.406'            │ 116278  │
│ 1       │ 'express4''5.157''4.771''12245.256'            │ 122593  │
└─────────┴────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyExpress4 vs express4 Speedup: 0.948x

Fastify Basic Get Request
┌─────────┬───────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                 │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyFastify''2.979''2.414''20821.892'            │ 208408  │
│ 1       │ 'fastify''2.690''2.124''22895.550'            │ 229044  │
│ 2       │ 'NestJSFastify''3.111''2.604''20020.216'            │ 200339  │
└─────────┴───────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyFastify vs fastify Speedup: 0.909x
currentInversifyFastify vs NestJSFastify Speedup: 1.040x

Hono Basic Get Request
┌─────────┬────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name              │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyHono''3.336''2.799''18679.679'            │ 186914  │
│ 1       │ 'hono''2.951''2.394''20969.801'            │ 209819  │
└─────────┴────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyHono vs hono Speedup: 0.891x

uWebSockets Basic Get Request
┌─────────┬───────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                     │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyUwebsockets''2.038''1.408''29690.016'            │ 296947  │
│ 1       │ 'uwebsockets''1.983''1.428''30604.556'            │ 306104  │
└─────────┴───────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyUwebsockets vs uwebsockets Speedup: 0.970x

The actual test case can't hardly be covered
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

Benchmarks results:

> root@ benchmarks:http:cjs:low /home/runner/work/monorepo/monorepo
> pnpm run --filter "@inversifyjs/http-benchmarks" benchmarks:run:cjs:low


> @inversifyjs/http-benchmarks@ benchmarks:run:cjs:low /home/runner/work/monorepo/monorepo/packages/framework/http/tools/http-benchmarks
> BUILD_TARGET=cjs VUS=128 ./bin/run-cjs.cjs

[CJS] Running benchmarks...

Express Basic Get Request
┌─────────┬───────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                 │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyExpress''5.419''5.017''11632.384'            │ 116506  │
│ 1       │ 'express''5.098''4.630''12368.322'            │ 123884  │
│ 2       │ 'NestJSExpress''5.623''5.162''11231.053'            │ 112441  │
└─────────┴───────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyExpress vs express Speedup: 0.940x
currentInversifyExpress vs NestJSExpress Speedup: 1.036x

Express v4 Basic Get Request
┌─────────┬────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                  │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyExpress4''5.374''5.059''11759.725'            │ 117676  │
│ 1       │ 'express4''5.325''5.060''11861.047'            │ 118758  │
└─────────┴────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyExpress4 vs express4 Speedup: 0.991x

Fastify Basic Get Request
┌─────────┬───────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                 │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyFastify''2.912''2.425''21229.797'            │ 212432  │
│ 1       │ 'fastify''2.702''2.178''22913.986'            │ 229303  │
│ 2       │ 'NestJSFastify''3.147''2.632''19764.773'            │ 197769  │
└─────────┴───────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyFastify vs fastify Speedup: 0.926x
currentInversifyFastify vs NestJSFastify Speedup: 1.074x

Hono Basic Get Request
┌─────────┬────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name              │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyHono''3.306''2.856''18815.120'            │ 188278  │
│ 1       │ 'hono''2.955''2.467''20977.526'            │ 209908  │
└─────────┴────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyHono vs hono Speedup: 0.897x

uWebSockets Basic Get Request
┌─────────┬───────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬─────────┐
│ (index) │ Task name                     │ Latency avg (ms) │ Latency med (ms) │ Throughput avg (req/s) │ Samples │
├─────────┼───────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼─────────┤
│ 0       │ 'currentInversifyUwebsockets''2.045''1.432''29577.038'            │ 295819  │
│ 1       │ 'uwebsockets''2.008''1.442''30239.769'            │ 302446  │
└─────────┴───────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴─────────┘
currentInversifyUwebsockets vs uwebsockets Speedup: 0.978x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant