-
-
Notifications
You must be signed in to change notification settings - Fork 23
Add swagger validator package #1735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
notaphplover
wants to merge
10
commits into
main
Choose a base branch
from
feat/add-swagger-validator-package
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8125620
feat(http-core): export getControllerMethodParameterMetadataList
notaphplover 65efc02
feat(http-open-api): expose openApiObject getter on SwaggerUiProvider
notaphplover 5b0ed4c
feat(http-open-api): export metadata utilities per version subpath
notaphplover 5be7986
feat(http-openapi-validation): add OpenAPI body validation package
notaphplover ce6d241
test(http-openapi-validation): add integration tests
notaphplover 78f956f
chore: add changesets for OpenAPI body validation feature
notaphplover 3591478
docs: add OpenAPI validation docs and code examples
notaphplover 58f5eb0
docs(blog): add OpenAPI body validation announcement post
notaphplover 0fbd5ed
chore: fix openapi validation package to valdiation packages
notaphplover d5e1698
chore: remove unused dependency
notaphplover File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@inversifyjs/http-core": minor | ||
| --- | ||
|
|
||
| Export `getControllerMethodParameterMetadataList` function and `ControllerMethodParameterMetadata` type from the public API. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@inversifyjs/http-open-api": minor | ||
| --- | ||
|
|
||
| Add `openApiObject` getter to `SwaggerUiProvider` (v3.1 and v3.2) to expose the populated OpenAPI spec object. Export `getControllerOpenApiMetadata`, `controllerOpenApiMetadataReflectKey`, and `ControllerOpenApiMetadata` from both version subpaths. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@inversifyjs/http-openapi-validation": minor | ||
| --- | ||
|
|
||
| Add `@inversifyjs/http-openapi-validation` package for OpenAPI-driven request body validation. Includes `@Validate()` parameter decorator and `OpenApiValidationPipe` for both OpenAPI v3.1 and v3.2, with Ajv-based schema validation and content-type resolution. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
...ices/inversify-framework-site/blog/2026-04-04-openapi-body-validation/index.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| --- | ||
| slug: openapi-body-validation-zero-extra-schemas | ||
| title: "OpenAPI Body Validation — Your Spec Is Your Schema" | ||
| authors: [notaphplover] | ||
| tags: [features] | ||
| --- | ||
| import apiIntroductionSource from '@inversifyjs/validation-code-examples/generated/examples/v1/openApiValidation/apiIntroduction.ts.txt'; | ||
| import CodeBlock from '@theme/CodeBlock'; | ||
|
|
||
| Raise your hand if you've ever copy-pasted a JSON schema from your OpenAPI spec into a validation layer. Now keep it raised if the two drifted apart within a week. | ||
|
|
||
| Yeah, we've been there too. | ||
|
|
||
| {/* truncate */} | ||
|
|
||
| ## The Problem Nobody Talks About | ||
|
|
||
| Every validation library asks you to define schemas. Ajv wants JSON Schema objects. Class-validator wants decorated classes. Zod wants its own DSL. And then your OpenAPI spec describes the **exact same shape** in `requestBody`. You end up maintaining two copies of the truth — and they inevitably disagree. | ||
|
|
||
| The result? Your Swagger UI happily shows one contract while your server enforces a completely different one. Bugs sneak in, clients get confused, and you spend Friday afternoon figuring out which version is "right." | ||
|
|
||
| ## What If Your OpenAPI Spec Was Enough? | ||
|
|
||
| That's the idea behind `@inversifyjs/http-openapi-validation`. If you're already decorating your controllers with `@OasRequestBody`, you've already written your validation schema — you just didn't know it. | ||
|
|
||
| This package reads the OpenAPI schemas you defined through `@inversifyjs/http-open-api`, compiles them with [Ajv](https://ajv.js.org/), and validates incoming request bodies automatically. **Zero extra schema definitions. Zero drift.** | ||
|
|
||
| ## See It in Action | ||
|
|
||
| Here's a complete working example — from defining the controller to wiring up validation: | ||
|
|
||
| <CodeBlock language="ts">{apiIntroductionSource}</CodeBlock> | ||
|
|
||
| That's it. The `@OasRequestBody` decorator defines the contract. The `@Validate()` decorator opts a parameter into validation. And `OpenApiValidationPipe` bridges the two by resolving schemas from the populated OpenAPI document. | ||
|
|
||
| When a request body doesn't match the spec, the pipe throws an `InversifyValidationError` with detailed Ajv error messages. The `InversifyValidationErrorFilter` catches it and responds with a `400 Bad Request` — exactly what your clients expect. | ||
|
|
||
| ## How It Works Under the Hood | ||
|
|
||
| 1. **Schema extraction** — When you call `swaggerProvider.provide(container)`, the `@OasRequestBody` metadata is merged into the OpenAPI document. `OpenApiValidationPipe` receives the fully populated spec. | ||
|
|
||
| 2. **JSON Pointer resolution** — For each request, the pipe resolves the matching operation, finds the `content` entry for the incoming `Content-Type`, and walks to the `schema` property using a JSON Pointer. | ||
|
|
||
| 3. **Lazy Ajv compilation** — Schemas are compiled into Ajv validators on first use and cached. Subsequent requests with the same schema skip compilation entirely. | ||
|
|
||
| 4. **Validation** — The compiled validator runs against the request body. On failure, an `InversifyValidationError` is thrown with all Ajv error details. | ||
|
|
||
| The whole pipeline adds negligible overhead to your request cycle — Ajv is one of the fastest JSON Schema validators available, and lazy compilation means cold starts don't pay the full cost upfront. | ||
|
|
||
| ## OpenAPI 3.1 and 3.2 | ||
|
|
||
| Both OpenAPI versions are supported through dedicated subpath exports: | ||
|
|
||
| ```typescript | ||
| // OpenAPI 3.1 | ||
| import { OpenApiValidationPipe, Validate } from '@inversifyjs/http-openapi-validation'; | ||
|
|
||
| // OpenAPI 3.2 | ||
| import { OpenApiValidationPipe, Validate } from '@inversifyjs/http-openapi-validation/v3Dot2'; | ||
| ``` | ||
|
|
||
| The API is identical across versions — only the underlying OpenAPI types differ. | ||
|
|
||
| ## Why This Matters | ||
|
|
||
| - **Single source of truth** — Your OpenAPI spec **is** your runtime validation. No duplication. | ||
| - **Automatic consistency** — Change an `@OasRequestBody` decorator, and validation updates instantly. No second file to remember. | ||
| - **Battle-tested validation engine** — Powered by Ajv with `ajv-formats`, supporting `email`, `uri`, `date-time`, and dozens more string formats out of the box. | ||
| - **Gradual adoption** — Add `@Validate()` to the parameters you want validated. Everything else stays untouched. | ||
| - **Framework-agnostic** — Works with Express, Fastify, Hono, uWebSockets.js, and any adapter supported by InversifyJS HTTP. | ||
|
|
||
| ## Getting Started | ||
|
|
||
| Install the package alongside its peer dependency: | ||
|
|
||
| ```bash | ||
| npm install @inversifyjs/http-openapi-validation ajv ajv-formats | ||
| ``` | ||
|
|
||
| Then head over to the [OpenAPI Validation documentation](../../validation/openapi/introduction) for a detailed walkthrough, API reference, and more examples. | ||
|
|
||
| If you're already using `@inversifyjs/http-open-api` to document your API, adding body validation is a five-minute change. And you'll never need to maintain a separate set of schemas again. | ||
|
|
||
| --- | ||
|
|
||
| Have questions or feedback? Open an issue on [GitHub](https://github.com/inversify/monorepo) or join the conversation on Discord. We'd love to hear how you're using it. |
4 changes: 4 additions & 0 deletions
4
packages/docs/services/inversify-framework-site/validation-docs/openapi/_category_.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "label": "OpenAPI", | ||
| "position": 5 | ||
| } |
82 changes: 82 additions & 0 deletions
82
packages/docs/services/inversify-framework-site/validation-docs/openapi/api.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| --- | ||
| sidebar_position: 2 | ||
| title: API | ||
| --- | ||
| import apiOpenApiValidationPipeSource from '@inversifyjs/validation-code-examples/generated/examples/v1/openApiValidation/apiOpenApiValidationPipe.ts.txt'; | ||
| import apiValidateDecoratorSource from '@inversifyjs/validation-code-examples/generated/examples/v1/openApiValidation/apiValidateDecorator.ts.txt'; | ||
| import CodeBlock from '@theme/CodeBlock'; | ||
|
|
||
| ## OpenApiValidationPipe | ||
|
|
||
| Validates incoming request bodies against the schemas defined in your OpenAPI specification. The pipe uses Ajv for JSON Schema validation and supports format validation via `ajv-formats`. | ||
|
|
||
| ```ts | ||
| constructor( | ||
| openApiObject: OpenApi3Dot1Object, | ||
| requestContentTypeProvider?: () => string | undefined, | ||
| ) | ||
| ``` | ||
|
|
||
| Parameters | ||
|
|
||
| - **openApiObject** (`OpenApi3Dot1Object` or `OpenApi3Dot2Object`): The populated OpenAPI document. Use `SwaggerUiProvider.openApiObject` to get a fully populated spec after calling `provide(container)`. | ||
| - **requestContentTypeProvider** (optional `() => string | undefined`): A function that returns the `Content-Type` header of the current request. When omitted, the pipe falls back to the single declared content type in the OpenAPI spec. | ||
|
|
||
| ### Schema resolution | ||
|
|
||
| The pipe constructs a JSON pointer to locate the schema for each request: | ||
|
|
||
| ``` | ||
| <spec-id>#/paths/<path>/<method>/requestBody/content/<content-type>/schema | ||
| ``` | ||
|
|
||
| The path and method are resolved from the controller metadata. The content type is resolved using the following strategy: | ||
|
|
||
| 1. If a `requestContentTypeProvider` is given and returns a value, use it. | ||
| 2. If the operation declares exactly one content type, use it as the fallback. | ||
| 3. Otherwise, throw an error — the content type is ambiguous. | ||
|
|
||
| ### Lazy initialization | ||
|
|
||
| The Ajv instance is created on the first call to `execute()`. This avoids compilation overhead during application bootstrap and ensures the OpenAPI spec is fully populated before schema compilation. | ||
|
|
||
| ### Example: register an OpenApiValidationPipe globally | ||
|
|
||
| <CodeBlock language="ts">{apiOpenApiValidationPipeSource}</CodeBlock> | ||
|
|
||
| ## Validate | ||
|
|
||
| Marks a controller method parameter for OpenAPI schema validation. Only parameters decorated with `@Validate()` are validated by the `OpenApiValidationPipe`. | ||
|
|
||
| ```ts | ||
| Validate(): ParameterDecorator | ||
| ``` | ||
|
|
||
| Apply `@Validate()` alongside `@Body()` on the parameter you want to validate: | ||
|
|
||
| ```ts | ||
| @Post('/') | ||
| public createUser(@Validate() @Body() user: User): string { | ||
| return `Created user: ${user.name}`; | ||
| } | ||
| ``` | ||
|
|
||
| The `@Validate()` decorator stores a boolean marker on the parameter index. During request processing, the `OpenApiValidationPipe` checks for this marker and skips parameters that aren't decorated. | ||
|
|
||
| ### Example: validate request body with OpenAPI schema | ||
|
|
||
| <CodeBlock language="ts">{apiValidateDecoratorSource}</CodeBlock> | ||
|
|
||
| ### Error Handling | ||
|
|
||
| When validation fails, the pipe throws an `InversifyValidationError` with a detailed message containing: | ||
| - The JSON Schema path where validation failed | ||
| - The instance path in the request body | ||
| - The validation error message from Ajv | ||
|
|
||
| For example: | ||
| ``` | ||
| [schema: #/properties/email/format, instance: /email]: "must match format \"email\"" | ||
| ``` | ||
|
|
||
| Use `InversifyValidationErrorFilter` (from `@inversifyjs/http-validation`) or a custom `@CatchError(InversifyValidationError)` filter to convert these errors into HTTP 400 responses. |
82 changes: 82 additions & 0 deletions
82
...docs/services/inversify-framework-site/validation-docs/openapi/introduction.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| --- | ||
| sidebar_position: 1 | ||
| title: Introduction | ||
| --- | ||
| import Tabs from '@theme/Tabs'; | ||
| import TabItem from '@theme/TabItem'; | ||
| import apiIntroductionSource from '@inversifyjs/validation-code-examples/generated/examples/v1/openApiValidation/apiIntroduction.ts.txt'; | ||
| import CodeBlock from '@theme/CodeBlock'; | ||
|
|
||
| The OpenAPI Validation package validates incoming request bodies against your existing [OpenAPI](https://www.openapis.org/) specification — no extra schema definitions needed. If you already document your API with `@OasRequestBody` decorators, this package reuses those schemas at runtime to ensure that every request matches the contract. | ||
|
|
||
| It uses [Ajv](https://ajv.js.org/) under the hood for fast, standards-compliant JSON Schema validation and supports both OpenAPI v3.1 and v3.2. | ||
|
|
||
| ## Install dependencies | ||
|
|
||
| Install the package along with its peer dependencies: | ||
|
|
||
| <Tabs> | ||
| <TabItem value="npm" label="npm"> | ||
|
|
||
| ```bash | ||
| npm install ajv ajv-formats @inversifyjs/http-openapi-validation | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem value="yarn" label="yarn"> | ||
|
|
||
| ```bash | ||
| yarn add ajv ajv-formats @inversifyjs/http-openapi-validation | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem value="pnpm" label="pnpm"> | ||
|
|
||
| ```bash | ||
| pnpm install ajv ajv-formats @inversifyjs/http-openapi-validation | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ## Quick Start | ||
|
|
||
| Here's a complete example showing OpenAPI-driven body validation: | ||
|
|
||
| <CodeBlock language="ts">{apiIntroductionSource}</CodeBlock> | ||
|
|
||
| This example: | ||
|
|
||
| 1. Defines an OpenAPI request body schema using `@OasRequestBody` on the controller method. | ||
| 2. Marks the `@Body()` parameter with `@Validate()` so the pipe knows to validate it. | ||
| 3. Uses `SwaggerUiProvider` to populate the OpenAPI spec from controller metadata. | ||
| 4. Creates an `OpenApiValidationPipe` from the populated spec and registers it globally. | ||
|
|
||
| When a request arrives, the pipe validates the body against the OpenAPI schema. Invalid requests are rejected with an `InversifyValidationError`, which the `InversifyValidationErrorFilter` converts into a 400 Bad Request response. | ||
|
|
||
| ## How It Works | ||
|
|
||
| The validation flow: | ||
|
|
||
| 1. **Schema discovery** — `SwaggerUiProvider.provide(container)` reads all `@OasRequestBody` decorators and populates the OpenAPI document with paths and schemas. | ||
| 2. **Pipe initialization** — `OpenApiValidationPipe` receives the populated OpenAPI document and lazily initializes an Ajv instance with the full spec on first use. | ||
| 3. **Request validation** — For each request, the pipe: | ||
| - Checks if the parameter is a `@Body()` marked with `@Validate()`. | ||
| - Resolves the content type (from the request header or a single declared type). | ||
| - Locates the matching schema using JSON pointer navigation. | ||
| - Validates the body against the schema using Ajv. | ||
| 4. **Error handling** — Validation failures throw `InversifyValidationError` with detailed error information. | ||
|
|
||
| ## OpenAPI Version Support | ||
|
|
||
| The package supports both OpenAPI v3.1 and v3.2 through subpath exports: | ||
|
|
||
| ```ts | ||
| // OpenAPI v3.1 (default) | ||
| import { OpenApiValidationPipe, Validate } from '@inversifyjs/http-openapi-validation'; | ||
|
|
||
| // OpenAPI v3.2 | ||
| import { OpenApiValidationPipe, Validate } from '@inversifyjs/http-openapi-validation/v3Dot2'; | ||
| ``` | ||
|
|
||
| Use the matching OpenAPI decorators and types from `@inversifyjs/http-open-api` or `@inversifyjs/http-open-api/v3Dot2`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
...fy-validation-code-examples/src/examples/v1/openApiValidation/apiIntroduction.int.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import { afterAll, beforeAll, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { SwaggerUiProvider } from '@inversifyjs/http-open-api'; | ||
| import { OpenApiValidationPipe } from '@inversifyjs/http-openapi-validation'; | ||
| import { InversifyValidationErrorFilter } from '@inversifyjs/http-validation'; | ||
| import { type OpenApi3Dot1Object } from '@inversifyjs/open-api-types/v3Dot1'; | ||
| import { Container } from 'inversify'; | ||
|
|
||
| import { buildExpressServer } from '../../../server/adapter/express/actions/buildExpressServer.js'; | ||
| import { type Server } from '../../../server/models/Server.js'; | ||
| import { UserController } from './apiIntroduction.js'; | ||
|
|
||
| describe('API Introduction (OpenAPI Validation)', () => { | ||
| describe('having an OpenApiValidationPipe in an HTTP server with Quick Start user validation', () => { | ||
| let server: Server; | ||
|
|
||
notaphplover marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| beforeAll(async () => { | ||
| const container: Container = new Container(); | ||
|
|
||
| const openApiObject: OpenApi3Dot1Object = { | ||
| info: { title: 'My API', version: '1.0.0' }, | ||
| openapi: '3.1.1', | ||
| }; | ||
|
|
||
| const swaggerProvider: SwaggerUiProvider = new SwaggerUiProvider({ | ||
| api: { | ||
| openApiObject, | ||
| path: '/docs', | ||
| }, | ||
| }); | ||
|
|
||
| container | ||
| .bind(InversifyValidationErrorFilter) | ||
| .toSelf() | ||
| .inSingletonScope(); | ||
| container.bind(UserController).toSelf().inSingletonScope(); | ||
|
|
||
| swaggerProvider.provide(container); | ||
|
|
||
| server = await buildExpressServer( | ||
| container, | ||
| [InversifyValidationErrorFilter], | ||
| [new OpenApiValidationPipe(swaggerProvider.openApiObject)], | ||
| ); | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| await server.shutdown(); | ||
| }); | ||
|
|
||
| describe('when a valid POST /users request is made', () => { | ||
| let response: Response; | ||
|
|
||
| beforeAll(async () => { | ||
| response = await fetch( | ||
| `http://${server.host}:${server.port.toString()}/users`, | ||
| { | ||
| body: JSON.stringify({ | ||
| email: 'jane.doe@example.com', | ||
| name: 'Jane Doe', | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| method: 'POST', | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it('should return expected Response', async () => { | ||
| expect(response.status).toBe(200); | ||
| await expect(response.text()).resolves.toBe('Created user: Jane Doe'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when an invalid POST /users request is made', () => { | ||
| let response: Response; | ||
|
|
||
| beforeAll(async () => { | ||
| response = await fetch( | ||
| `http://${server.host}:${server.port.toString()}/users`, | ||
| { | ||
| body: JSON.stringify({ | ||
| email: 'jane.doe@example.com', | ||
| extra: 'not allowed', | ||
| name: 'Jane Doe', | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| method: 'POST', | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it('should return expected Response', async () => { | ||
| expect(response.status).toBe(400); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.