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
export class CreateOrderDto {
status: 'pending' | 'shipped' | 'delivered';
}nestjs-openapi
{
"type": "string",
"enum": ["pending", "shipped", "delivered"]
}@nestjs/swagger
{
"type": "object"
}Fix: add
enum option to decoratorGeneric Types
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 usageInterfaces
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 optionNullable Types
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 optionEnums
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 decoratorDiscriminated Unions
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 decoratorUnion of Objects
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 allInterface Return Types
// 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/swagger | nestjs-openapi | |
|---|---|---|
| Best for | Full decorator control, OpenAPI 3.1 | TypeScript-first, CI/CD pipelines |
| Requires | App bootstrap, infrastructure | Just source files |
| Schema source | Decorators | TypeScript types |
See the Migration Guide for running both in parallel.
Feature Parity
Decorator Support
| Category | Decorator | Status |
|---|---|---|
| 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
| Decorator | Status |
|---|---|
@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/swagger | nestjs-openapi |
|---|---|---|
| Approach | Runtime reflection | Static AST analysis |
| Requires running app | Yes | No |
| Build-time generation | No | Yes |
| Effect on bundle size | Adds runtime code | Zero runtime |
| CI/CD friendly | Needs app bootstrap | Just npx |
Summary
| Category | Parity |
|---|---|
| HTTP routing | 100% |
| Security decorators | 100% |
| Operation metadata | ~95% |
| Schema generation | ~80% |
| class-validator | ~95% |
| Advanced features | ~50% |
Overall: ~85-90% feature parity for typical REST API documentation needs.