Docs are a work in progress - contributions welcome
Logonestjs-openapi

Comparison

See how static analysis produces better OpenAPI schemas than runtime reflection.

@nestjs/swagger relies on reflect-metadata at runtime, which fundamentally limits what it can see. Here's how the same TypeScript produces different OpenAPI schemas.

Union Types

Input
export class CreateOrderDto {
  status: 'pending' | 'shipped' | 'delivered';
}
nestjs-openapi
{
  "type": "string",
  "enum": ["pending", "shipped", "delivered"]
}
@nestjs/swagger
{
  "type": "object"
}
Fix: add enum option to decorator

Generic Types

Input
class PaginatedResponse<T> {
  items: T[];
  total: number;
}

// Return type: PaginatedResponse<UserDto>
nestjs-openapi
{
  "items": {
    "type": "array",
    "items": {
      "$ref": "#/components/schemas/UserDto"
    }
  }
}
@nestjs/swagger
{
  "items": {
    "type": "array"
  }
}
Fix: add type option on every usage

Interfaces

Input
interface Address {
  street: string;
  city: string;
}

export class UserDto {
  address: Address;
}
nestjs-openapi
{
  "address": {
    "$ref": "#/components/schemas/Address"
  }
}
@nestjs/swagger
{
  "address": {
    "type": "object"
  }
}
Fix: convert to class + add type option

Nullable Types

Input
export class UpdateUserDto {
  name?: string; // optional
  bio: string | null; // nullable
}
nestjs-openapi
{
  "name": {
    "type": "string"
  },
  "bio": {
    "type": "string",
    "nullable": true
  },
  "required": ["bio"]
}
@nestjs/swagger
{
  "name": {
    "type": "object"
  },
  "bio": {
    "type": "object"
  }
}
Fix: use @ApiPropertyOptional() + nullable option

Enums

Input
enum Role {
  Admin = 'admin',
  User = 'user',
}

export class UserDto {
  role: Role;
}
nestjs-openapi
{
  "type": "string",
  "enum": ["admin", "user"]
}
@nestjs/swagger
{
  "type": "object"
}
Fix: add enum option to decorator

Discriminated Unions

Input
type ApiResponse =
  | { status: 'success'; data: UserDto }
  | { status: 'error'; message: string };

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id') id: string): ApiResponse {
    // ...
  }
}
nestjs-openapi
{
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "status": { "const": "success" },
        "data": { "$ref": "#/components/schemas/UserDto" }
      }
    },
    {
      "type": "object",
      "properties": {
        "status": { "const": "error" },
        "message": { "type": "string" }
      }
    }
  ]
}
@nestjs/swagger
{
  "type": "object"
}
Fix: create wrapper class + use @ApiExtraModels + oneOf in decorator

Union of Objects

Input
export class CatDto {
  name: string;
  meow: boolean;
}

export class DogDto {
  name: string;
  bark: boolean;
}

export class PetOwnerDto {
  ownerName: string;
  pet: CatDto | DogDto; // Union of different object types
}
nestjs-openapi
{
  "PetOwnerDto": {
    "properties": {
      "ownerName": { "type": "string" },
      "pet": {
        "anyOf": [
          { "$ref": "#/components/schemas/CatDto" },
          { "$ref": "#/components/schemas/DogDto" }
        ]
      }
    }
  }
}
Both CatDto and DogDto schemas are also generated
@nestjs/swagger
{
  "PetOwnerDto": {
    "type": "object",
    "properties": {}
  }
}
CatDto and DogDto not generated at all

Interface Return Types

Input
// Interface, not a class
export interface IUserProfile {
  id: string;
  username: string;
  displayName: string;
  avatarUrl: string | null;
}

@Controller('profile')
export class ProfileController {
  @Get(':id')
  getProfile(@Param('id') id: string): IUserProfile {
    // ...
  }
}
nestjs-openapi
{
  "IUserProfile": {
    "properties": {
      "id": { "type": "string" },
      "username": { "type": "string" },
      "displayName": { "type": "string" },
      "avatarUrl": { "type": ["string", "null"] }
    }
  }
}
Interface fully resolved with nullable type preserved
@nestjs/swagger
{
  "type": "object"
}
Interfaces are invisible to runtime reflection

The Decorator Tax

With @nestjs/swagger, accurate schemas require duplicating type information:

export class CreateOrderDto {
  @ApiProperty({ enum: ['pending', 'shipped', 'delivered'] })
  status: 'pending' | 'shipped' | 'delivered';

  @ApiProperty({ type: () => AddressDto })
  address: Address;

  @ApiProperty({ type: [ItemDto] })
  items: ItemDto[];

  @ApiProperty({ nullable: true })
  notes: string | null;
}

With nestjs-openapi, TypeScript is the source of truth-no duplication, no drift.


When to Use Each

@nestjs/swaggernestjs-openapi
Best forFull decorator control, OpenAPI 3.1TypeScript-first, CI/CD pipelines
RequiresApp bootstrap, infrastructureJust source files
Schema sourceDecoratorsTypeScript types

See the Migration Guide for running both in parallel.


Feature Parity

Decorator Support

CategoryDecoratorStatus
Routing@Controller, @Get/@Post/@Put/@Patch/@Delete
@Options/@Head/@All
@Param/@Query/@Body/@Headers
@HttpCode
Operation@ApiTags
@ApiOperation
@ApiResponse
@ApiParam/@ApiQuery/@ApiBody/@ApiHeader
@ApiConsumes/@ApiProduces
@ApiExcludeEndpoint/@ApiExcludeController
Security@ApiBearerAuth
@ApiBasicAuth
@ApiOAuth2
@ApiSecurity
@ApiCookieAuth
Schema@ApiProperty⚠️ Use TypeScript types
@ApiPropertyOptional⚠️ Use ? syntax
@ApiHideProperty
Other@ApiExtension
@ApiExtraModels
Response shortcuts (@ApiOkResponse, etc.)❌ Use @ApiResponse

class-validator Support

DecoratorStatus
@IsString/@IsNumber/@IsBoolean/@IsArray
@IsEmail/@IsUrl/@IsUUID/@IsDate
@MinLength/@MaxLength/@Length
@Min/@Max/@IsPositive/@IsNegative
@ArrayMinSize/@ArrayMaxSize
@Matches (regex)
@IsOptional
@IsEnum⚠️ Recognized, values not extracted

Architecture Comparison

Aspect@nestjs/swaggernestjs-openapi
ApproachRuntime reflectionStatic AST analysis
Requires running appYesNo
Build-time generationNoYes
Effect on bundle sizeAdds runtime codeZero runtime
CI/CD friendlyNeeds app bootstrapJust npx

Summary

CategoryParity
HTTP routing100%
Security decorators100%
Operation metadata~95%
Schema generation~80%
class-validator~95%
Advanced features~50%

Overall: ~85-90% feature parity for typical REST API documentation needs.

On this page