Skip to main content

OpenAPI Validation — Your Spec Is Your Schema

· 5 min read
Roberto Pintos López
InversifyJS maintainer

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.

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/open-api-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, and validates incoming request bodies and headers 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:

import { Controller, Post } from '@inversifyjs/http-core';
import { InversifyExpressHttpAdapter } from '@inversifyjs/http-express';
import { OasRequestBody, SwaggerUiProvider } from '@inversifyjs/http-open-api';
import { InversifyValidationErrorFilter } from '@inversifyjs/http-validation';
import { type OpenApi3Dot1Object } from '@inversifyjs/open-api-types/v3Dot1';
import { ValidatedBody } from '@inversifyjs/open-api-validation';
import { OpenApiValidationPipe } from '@inversifyjs/open-api-validation/v3Dot1';
import { Container } from 'inversify';

const container: Container = new Container();

// Define a base OpenAPI document
const openApiObject: OpenApi3Dot1Object = {
info: { title: 'My API', version: '1.0.0' },
openapi: '3.1.1',
};

// Create the SwaggerUiProvider
const swaggerProvider: SwaggerUiProvider = new SwaggerUiProvider({
api: {
openApiObject,
path: '/docs',
},
});

interface User {
email: string;
name: string;
}

@Controller('/users')
export class UserController {
@OasRequestBody({
content: {
'application/json': {
schema: {
additionalProperties: false,
properties: {
email: { format: 'email', type: 'string' },
name: { minLength: 1, type: 'string' },
},
required: ['name', 'email'],
type: 'object',
},
},
},
})
@Post('/')
public createUser(@ValidatedBody() user: User): string {
return `Created user: ${user.name}`;
}
}

// Register bindings
container.bind(InversifyValidationErrorFilter).toSelf().inSingletonScope();
container.bind(UserController).toSelf().inSingletonScope();

// Populate the OpenAPI spec from controller metadata
swaggerProvider.provide(container);

// Create HTTP adapter
const adapter: InversifyExpressHttpAdapter = new InversifyExpressHttpAdapter(
container,
);

// Register global validation pipe using the populated OpenAPI spec
adapter.useGlobalPipe(new OpenApiValidationPipe(swaggerProvider.openApiObject));
adapter.useGlobalFilters(InversifyValidationErrorFilter);

That's it. The @OasRequestBody decorator defines the contract. The @ValidatedBody() decorator extracts the body along with request context and marks it for 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.

Header Validation Too

Body validation is only half the story. APIs often receive critical input through headers — API keys, pagination parameters, request IDs. The same "two sources of truth" problem applies: your @OasParameter decorators define the header contract in the OpenAPI spec, but nothing validates them at runtime.

With @ValidatedHeaders(), the pipe validates incoming headers against the schemas defined in your @OasParameter({ in: 'header' }) decorators. String header values are automatically coerced to the expected type (integer, number, boolean, etc.) before validation.

import { Controller, Get } from '@inversifyjs/http-core';
import { OasParameter } from '@inversifyjs/http-open-api';
import { ValidatedHeaders } from '@inversifyjs/open-api-validation';

interface ResourceHeaders {
'x-page-size'?: number;
'x-request-id': string;
}

@Controller('/resources')
export class ResourceController {
@OasParameter({
in: 'header',
name: 'x-request-id',
required: true,
schema: { type: 'string' },
})
@OasParameter({
in: 'header',
name: 'x-page-size',
required: false,
schema: { minimum: 1, type: 'integer' },
})
@Get('/')
public getResources(@ValidatedHeaders() headers: ResourceHeaders): string {
return `Request ID: ${headers['x-request-id']}`;
}
}

Headers are validated individually against their own schemas. When a required header is missing or a header value doesn't match its schema, the pipe throws the same InversifyValidationError — keeping the error handling consistent across body and header validation.

How It Works Under the Hood

  1. Schema extraction — When you call swaggerProvider.provide(container), the @OasRequestBody and @OasParameter 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 body validation or the header parameter schemas for header validation, 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 validators run against the request body and/or headers. 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:

// Decorators (version-agnostic)
import { ValidatedBody, ValidatedHeaders } from '@inversifyjs/open-api-validation';

// OpenAPI 3.1 pipe
import { OpenApiValidationPipe } from '@inversifyjs/open-api-validation/v3Dot1';

// OpenAPI 3.2 pipe
import { OpenApiValidationPipe } from '@inversifyjs/open-api-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 @ValidatedBody() or @ValidatedHeaders() 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:

npm install @inversifyjs/open-api-validation ajv ajv-formats

Then head over to the OpenAPI Validation documentation 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 or join the conversation on Discord. We'd love to hear how you're using it.