Akronae/nestjs-openapi-validation(opens in a new tab)
Language: TypeScript
✅ Validate NestJS DTOs with Zod using TypeScript/OpenAPI spec.
- stars
- 16
- watchers
- 0
- forks
- 2
- open issues
- 0
- Last updated
- 09/10/2026
Language: TypeScript
✅ Validate NestJS DTOs with Zod using TypeScript/OpenAPI spec.
Stop repeating yourself. Validate your NestJS applications using only TypeScript and OpenAPI.
In a standard NestJS project, defining a single property often requires you to write it three times:
id: number; (For type safety)@IsInt() (For class-validator)@ApiProperty() (For Swagger/OpenAPI)This is redundant, error-prone, and a nightmare to maintain. nestjs-openapi-validation uses TypeScript inference to automatically generate validation rules and OpenAPI schemas. For most properties, the TypeScript definition is enough.
You have to manually sync decorators for every single field. Missing one @Expose() or @IsNumber() breaks your API or documentation.
class Item { @Expose() // Needed for response transformation @Type(() => Number) // Needed to ensure it's a number @IsNumber() // Validation @Min(0) // Logic @ApiProperty({ minimum: 0 }) // Documentation size?: number; }
The library infers the validation and documentation from your types and the @OpenApiRegister() decorator.
@OpenApiRegister() class Item { @ApiProperty({ minimum: 0 }) // Only add what can't be inferred size?: number; }
passwordHash, internalNotes) never leak# Using npm npm install nestjs-openapi-validation zod # Using yarn yarn add nestjs-openapi-validation zod # Using pnpm pnpm add nestjs-openapi-validation zod
// package.json { "scripts": { "dev": "nest start --watch --type-check", }, }
// nest-cli.json { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "builder": { "type": "swc", "options": { "stripLeadingPaths": false, "includeDotfiles": true, }, }, "deleteOutDir": true, "plugins": [ { "name": "@nestjs/swagger", "options": { "introspectComments": true, }, }, ], }, }
// main.ts import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; // This is automatically generated thanks to @nestjs/swagger plugin // defined in nest-cli.json import metadata from './metadata'; import { AppModule } from './app.module'; import { OpenApiValidationInterceptor, OpenApiValidationPipe, getRegisteredOpenApiModels, } from 'nestjs-openapi-validation'; async function bootstrap() { const app = await NestFactory.create(AppModule); const config = new DocumentBuilder() .setTitle('My API') .setDescription('API with automatic validation') .setVersion('1.0') .build(); await SwaggerModule.loadPluginMetadata(metadata); const document = SwaggerModule.createDocument(app, config, { // Include models registered with @OpenApiRegister() extraModels: getRegisteredOpenApiModels(), }); // Validate user input app.useGlobalPipes(new OpenApiValidationPipe(metadata, document)); // Validate responses app.useGlobalInterceptors( new OpenApiValidationInterceptor(metadata, document), ); await app.listen(3000); }
// user.dto.ts import { ApiProperty } from '@nestjs/swagger'; import { OpenApiRegister } from 'nestjs-openapi-validation'; export enum UserRole { ADMIN = 'admin', USER = 'user', MODERATOR = 'moderator', } export class CreateUserDto { @ApiProperty({ minLength: 2, maxLength: 50 }) name: string; @ApiProperty({ format: 'email' }) email: string; @ApiProperty({ minimum: 18, maximum: 120 }) age: number; @ApiProperty({ enum: UserRole }) role: UserRole; @ApiProperty({ format: 'url' }) website?: string; @ApiProperty({ pattern: /^[0-9]{10}$/.source }) phone: string; tags?: string[]; } // For DTOs not automatically discovered by the Swagger plugin @OpenApiRegister() export class SpecialUserDto { @ApiProperty({ minimum: 0, maximum: 120 }) age?: number; name: string; }
// user.controller.ts import { Controller, Post, Body } from '@nestjs/common'; import { CreateUserDto } from './user.dto'; @Controller('users') export class UserController { @Post() createUser(@Body() userData: CreateUserDto) { // userData is automatically validated against your OpenAPI schema! return { message: 'User created', data: userData }; } }
enum Status { active = 'ACTIVE', inactive = 'INACTIVE', } export class FlexibleQuery { @ApiProperty({ oneOf: [{ enum: Object.values(Status) }, { type: 'string' }], }) status: $`{Status}` | (string & {}); @ApiProperty({ oneOf: [ { $ref: getSchemaPath(UserDto) }, { $ref: getSchemaPath(AdminDto) } ] }) profile: UserDto | AdminDto; }
Complexe types are supported out of the box without decorators.
export class OrderDto { items: OrderItemDto[]; customer: CustomerDto; orderDate: Date; stringMatrix: string[][]; deeply: { nested { value: true; } } }
export class ProductDto { @ApiProperty({ minLength: 3, maxLength: 100, pattern: /^[a-zA-Z0-9\s-]+$/.source, }) name: string; @ApiProperty({ minimum: 0.01, maximum: 1000000, }) price: number; @ApiProperty({ minItems: 1, maxItems: 10, items: { type: 'string', maxLength: 50 }, }) tags: string[]; @ApiProperty({ format: 'url' }) imageUrl: string; }
Check src/modules/app/app.dto.ts to see all tested features.
If you see an error like:
YourDto is not registered in your OpenAPI document. Use `@OpenApiRegister()` on your DTO to register it.
it means the DTO is not automatically discovered by the NestJS Swagger plugin. This commonly happens when the DTO is only referenced in unions (oneOf), used in dynamic contexts, lives outside of scanned modules, or is only used as an input query and never returned.
Use the @OpenApiRegister() decorator on such DTOs, and pass the registered models to Swagger via extraModels:
Use the @OpenApiIgnore() to skip specific model validation.
import { ApiProperty } from '@nestjs/swagger'; import { OpenApiRegister, getRegisteredOpenApiModels, } from 'nestjs-openapi-validation'; @OpenApiRegister() export class CustomDto { name: string; @ApiProperty({ minimum: 0, maximum: 120 }) age?: number; } // main.ts (excerpt) const document = SwaggerModule.createDocument(app, config, { extraModels: getRegisteredOpenApiModels(), });
Once registered, request/response validation will work as expected for these models.
| OpenAPI Feature | Supported | Zod Equivalent |
|---|---|---|
type: string |
✅ | z.string() |
type: number |
✅ | z.coerce.number() |
type: boolean |
✅ | z.coerce.boolean() |
type: array |
✅ | z.array() |
enum |
✅ | z.enum() |
oneOf |
✅ | z.union() |
$ref |
✅ | Nested schemas |
format: email |
✅ | z.string().email() |
format: url |
✅ | z.string().url() |
format: date-time |
✅ | z.string().datetime() |
minimum/maximum |
✅ | z.number().min().max() |
minLength/maxLength |
✅ | z.string().min().max() |
pattern |
✅ | z.string().regex() |
required fields |
✅ | Required vs optional |
Both OpenApiValidationPipe and OpenApiValidationInterceptor accept two parameters:
@nestjs/swagger CLI pluginnew OpenApiValidationPipe(metadata, document); new OpenApiValidationInterceptor(metadata, document);
On validation failures, the library returns HTTP 400 (Bad Request) with Zod errors grouped by location (e.g., query, body, or response). Each group contains an issues array describing the problems.
// Example error response (request validation) { "error": { "query": { "issues": [ { "code": "invalid_type", "expected": "string", "message": "required", "path": ["str1"], "received": "undefined", }, { "code": "invalid_string", "message": "Invalid datetime", "path": ["date"], "validation": "datetime", }, ], }, }, }
The same structure applies to response validation, where issues would be reported under response.
The library includes comprehensive tests covering all validation scenarios:
# Run tests # Using yarn yarn test # Using npm npm test # Using pnpm pnpm test
Example test case:
it('should validate email format', async () => { const res = await request(app.getHttpServer()).post('/users').send({ name: 'John Doe', email: 'invalid-email', // This will fail age: 25, }); expect(res.status).toBe(400); expect(res.body.message).toContain('Invalid email'); });
# Clone the repository git clone https://github.com/akronae/nestjs-openapi-validation.git # Install dependencies yarn install # Build the library yarn lib:build # Run tests yarn test # Start development server yarn dev
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
This project is MIT licensed.
⭐ Star this repo if you find it useful!
Made with ❤️ for the NestJS community