Skip to content
NestJS ns openapi 4 min read

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.

OptionTypePurpose
typeType | Function | [Type]Explicit type when inference is impossible (e.g. arrays, generics)
exampleanySample value shown in Swagger UI and the spec
descriptionstringHuman-readable explanation of the field
requiredbooleanWhether the field must be present (default true)
nullablebooleanAllow an explicit null value alongside the type
enumobject | arrayRestrict the field to a fixed set of values
defaultanyDefault applied when the field is omitted
minimum / maximumnumberNumeric bounds rendered in the schema
isArraybooleanMark 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 @ApiProperty only where you need a custom example, description, or constraint.
  • Always set enumName for enums so the spec emits one shared definition instead of inlining duplicates.
  • Use type: () => OtherDto thunks 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 @ApiProperty with class-validator decorators so the documented constraints match what validation actually enforces.
  • Provide realistic example values — they populate Swagger UI’s “Try it out” forms and make the docs self-demonstrating.
Last updated June 14, 2026
Was this helpful?