DTO Schemas & ApiProperty
OpenAPI is only useful when consumers can see the exact shape of the data your endpoints accept and return. In NestJS that shape comes from your DTO classes, but TypeScript interfaces and types vanish at compile time, so @nestjs/swagger cannot read them. The @ApiProperty decorator restores that lost metadata at runtime — describing each field’s type, example, and constraints — and the Swagger CLI plugin can infer most of it automatically. This page covers documenting fields by hand, enabling the plugin, and modeling nested objects, arrays, and enums.
Why DTOs need decorators
When the compiler emits JavaScript, the property types on a class are erased. @nestjs/swagger reads metadata through the reflect-metadata API, and only decorated properties leave a trace. A plain DTO therefore produces an empty schema — Swagger UI shows the model name but no fields. Adding @ApiProperty() to each property registers it in the OpenAPI schema with its type, requiredness, and any extra hints you supply.
// src/cats/dto/create-cat.dto.ts
import { ApiProperty } from '@nestjs/swagger';
export class CreateCatDto {
@ApiProperty({ example: 'Whiskers', description: 'The cat’s display name' })
name: string;
@ApiProperty({ example: 3, minimum: 0, maximum: 30 })
age: number;
@ApiProperty({ required: false, example: 'Tabby' })
breed?: string;
}
A controller that consumes this DTO needs no extra annotation — the type in the method signature links the route body to the schema.
// src/cats/cats.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
@Controller('cats')
export class CatsController {
@Post()
create(@Body() createCatDto: CreateCatDto) {
return { id: 1, ...createCatDto };
}
}
Common @ApiProperty options
@ApiProperty accepts an options object that maps directly onto OpenAPI schema keywords. Use @ApiPropertyOptional() as shorthand for required: false.
| Option | Type | Purpose |
|---|---|---|
type | Type | Function | [Type] | Explicit type when inference is impossible (e.g. arrays, generics) |
example | any | Sample value shown in Swagger UI and the spec |
description | string | Human-readable explanation of the field |
required | boolean | Whether the field must be present (default true) |
nullable | boolean | Allow an explicit null value alongside the type |
enum | object | array | Restrict the field to a fixed set of values |
default | any | Default applied when the field is omitted |
minimum / maximum | number | Numeric bounds rendered in the schema |
isArray | boolean | Mark the property as an array of type |
Mark optional fields with
required: false(or@ApiPropertyOptional) rather than relying on the?modifier. The TypeScript modifier is erased at runtime, so without it Swagger reports every property as required.
Inferring metadata with the CLI plugin
Decorating every field is repetitive. The Swagger CLI plugin hooks into the TypeScript compiler, reads your DTOs’ types and ? modifiers, and generates the @ApiProperty metadata for you — so you only add decorators when you want a custom example, description, or constraint. Enable it in nest-cli.json.
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"introspectComments": true,
"dtoFileNameSuffix": [".dto.ts", ".entity.ts"]
}
}
]
}
}
With introspectComments on, the plugin even turns a property’s JSDoc comment into its description. After enabling it, the earlier DTO can drop most decorators:
// src/cats/dto/create-cat.dto.ts
export class CreateCatDto {
/** The cat's display name */
name: string;
age: number;
breed?: string; // inferred as optional from the ? modifier
}
Output:
$ nest start
[Nest] LOG [SwaggerModule] CreateCatDto schema generated: name (string, required), age (number, required), breed (string, optional)
Modeling nested and enum types
Arrays and nested objects need an explicit type, because reflection cannot recover the element type of an array. Pass a thunk (() => Type) for nested DTOs to avoid circular-import issues, and use the enum option for fixed value sets.
// src/cats/dto/create-cat.dto.ts
import { ApiProperty } from '@nestjs/swagger';
export enum CatStatus {
Available = 'available',
Adopted = 'adopted',
}
export class VaccinationDto {
@ApiProperty({ example: 'Rabies' })
name: string;
@ApiProperty({ example: '2025-03-01', format: 'date' })
givenOn: string;
}
export class CreateCatDto {
@ApiProperty({ example: 'Whiskers' })
name: string;
@ApiProperty({ enum: CatStatus, enumName: 'CatStatus', default: CatStatus.Available })
status: CatStatus;
@ApiProperty({ type: () => [VaccinationDto] })
vaccinations: VaccinationDto[];
@ApiProperty({ type: String, isArray: true, example: ['indoor', 'friendly'] })
tags: string[];
@ApiProperty({ type: String, nullable: true, example: null })
microchipId: string | null;
}
Setting enumName is important: without it, NestJS inlines the enum into every schema that references it, producing duplicate definitions. Naming it creates a single reusable $ref in the spec.
Best practices
- Enable the CLI plugin first, then add
@ApiPropertyonly where you need a custom example, description, or constraint. - Always set
enumNamefor enums so the spec emits one shared definition instead of inlining duplicates. - Use
type: () => OtherDtothunks for nested DTOs to sidestep circular-dependency errors. - Keep request and response DTOs separate; never expose entity fields like password hashes through an
@ApiProperty. - Pair
@ApiPropertywithclass-validatordecorators so the documented constraints match what validation actually enforces. - Provide realistic
examplevalues — they populate Swagger UI’s “Try it out” forms and make the docs self-demonstrating.