Автоматическая документация API

Автоматическая генерация документации API в экосистемах, использующих class-validator, строится вокруг идеи повторного использования декларативных DTO-классов как единого источника правды. Валидаторы описывают ограничения данных, а инструменты документирования интерпретируют эти ограничения и превращают их в схему OpenAPI или аналогичную спецификацию.

class-validator описывает структуру входных данных через декораторы:

  • ограничения типов
  • обязательность полей
  • диапазоны значений
  • правила формата

Эти же характеристики являются основой API-контракта. Например, поле, помеченное как обязательное и строковое, в документации должно отображаться как required: true и type: string.

import { IsString, IsNotEmpty, IsEmail } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsEmail()
  email: string;
}

На уровне логики это уже описание структуры запроса, но без отдельного слоя документации.

Ограничения class-validator как источника документации

class-validator сам по себе не генерирует OpenAPI-схемы. Он хранит метаданные, которые:

  • используются при валидации в runtime
  • доступны через reflect-metadata
  • не имеют стандартизированного отображения в API-форматах

Поэтому автоматическая документация почти всегда требует дополнительного слоя, который интерпретирует эти метаданные.

NestJS и интеграция со Swagger

В экосистеме NestJS основной механизм документации — пакет @nestjs/swagger. Он строит OpenAPI-схему, анализируя DTO-классы.

import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsInt, Min } from 'class-validator';

export class CreateProductDto {
  @ApiProperty({ example: 'Phone' })
  @IsString()
  name: string;

  @ApiProperty({ example: 10 })
  @IsInt()
  @Min(0)
  price: number;
}

Здесь происходит разделение ответственности:

  • class-validator описывает правила валидации
  • @nestjs/swagger описывает документацию

Частичное перекрытие метаданных

Некоторые правила можно интерпретировать автоматически, но не все:

Декоратор Возможная Swagger-интерпретация
@IsString() type: string
@IsInt() type: integer
@IsEmail() format: email
@IsNotEmpty() required (косвенно)
@Min() / @Max() ограничения чисел
@Length() ограничения строки

Однако NestJS не использует class-validator как единственный источник — он требует явного @ApiProperty, потому что не все ограничения однозначно мапятся.

reflect-metadata и роль типизации

Автоматическая документация в TypeScript-проектах зависит от включённого параметра:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  }
}

Без этого метаданные типов недоступны, и генерация схем становится неполной.

Пример, где тип извлекается автоматически:

class UserDto {
  @IsString()
  name: string;
}

Тип string может быть извлечён из metadata, но только для базовых случаев.

Сложные типы и вложенные DTO

Наибольшие сложности возникают при вложенных структурах:

import { ValidateNested, IsArray } from 'class-validator';
import { Type } from 'class-transformer';

class AddressDto {
  @IsString()
  city: string;
}

class UserDto {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => AddressDto)
  addresses: AddressDto[];
}

Для документации это означает:

  • массив объектов
  • ссылка на вложенную схему AddressDto
  • необходимость рекурсивного анализа

Swagger-интеграции должны явно знать тип через @Type() или @ApiProperty({ type: ... }), иначе схема становится any.

Маппинг валидаторов в OpenAPI-схему

Типичная система генерации документации строит соответствия между декораторами и JSON Schema.

Строки

  • @IsString()type: string
  • @Length(min, max)minLength, maxLength
  • @Matches(regex)pattern

Числа

  • @IsInt()type: integer
  • @Min()minimum
  • @Max()maximum

Булевы значения

  • @IsBoolean()type: boolean

Email и форматы

  • @IsEmail()format: email
  • @IsUUID()format: uuid

Массивы

  • @IsArray()type: array
  • @ArrayMinSize()minItems
  • @ArrayMaxSize()maxItems

Перечисления

import { IsEnum } from 'class-validator';

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

class UserDto {
  @IsEnum(Role)
  role: Role;
}

В OpenAPI это становится:

  • type: string
  • enum: ['admin', 'user']

NestJS Swagger и автоматическое расширение схем

NestJS позволяет комбинировать автоматическую и ручную документацию:

@ApiProperty({
  description: 'Имя пользователя',
  minLength: 2,
  maxLength: 50,
})
@IsString()
@Length(2, 50)
name: string;

В этом случае class-validator и @ApiProperty дублируют информацию, но служат разным целям:

  • валидаторы обеспечивают runtime-ограничения
  • Swagger формирует контракт

Проблема неполной автоматизации

Полная автоматизация сталкивается с ограничениями:

  1. Декораторы валидации не всегда однозначны для схемы
  2. Runtime-метаданные могут быть потеряны после компиляции
  3. Условная логика валидаторов не выражается в OpenAPI
  4. Кастомные валидаторы не имеют стандартного отображения

Пример кастомного валидатора:

import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';

@ValidatorConstraint()
class IsOddConstraint implements ValidatorConstraintInterface {
  validate(value: number) {
    return value % 2 === 1;
  }
}

Такое правило невозможно напрямую выразить в JSON Schema без расширений.

routing-controllers и автоматическая OpenAPI генерация

Более тесная интеграция достигается в routing-controllers и routing-controllers-openapi.

import { JsonController, Post, Body } from 'routing-controllers';

@JsonController()
class UserController {
  @Post('/users')
  create(@Body() body: CreateUserDto) {
    return body;
  }
}

При включении OpenAPI-генерации:

  • DTO анализируются автоматически
  • class-validator используется как источник ограничений
  • схема строится без ручных @ApiProperty

TypeScript-first подход (tsoa)

tsoa использует другой подход:

  • типы TypeScript как основной источник
  • class-validator — дополнительный слой runtime-валидации
class UserDto {
  name: string;
}

Документация строится из типов, а валидаторы добавляют ограничения поверх схемы.

Проблема потери типов на уровне наследования

При сложных иерархиях классов возникают проблемы:

  • дублирование полей
  • невозможность корректного объединения схем
  • потеря метаданных при композиции классов
class BaseDto {
  @IsString()
  id: string;
}

class UserDto extends BaseDto {
  @IsString()
  name: string;
}

Некоторые генераторы корректно наследуют схему, другие требуют явного описания.

Кастомные декораторы и расширение схем

Создание собственных валидаторов часто требует синхронизации с документацией.

function IsPhone() {
  return function (target: any, propertyKey: string) {
    // class-validator metadata
  };
}

Без дополнительного слоя такие декораторы не попадают в OpenAPI, поэтому часто вводится отдельная регистрация схемы.

Проблемы вложенных и циклических структур

Циклические DTO создают сложности:

class A {
  @ValidateNested()
  b: B;
}

class B {
  @ValidateNested()
  a: A;
}

Генераторы документации должны:

  • разрывать циклы через $ref
  • выносить схемы в отдельные компоненты
  • избегать бесконечной рекурсии

Согласованность API-контракта

В системах, использующих class-validator, стабильная документация достигается только при соблюдении архитектурного принципа:

  • DTO-классы являются единственным источником структуры данных
  • валидаторы описывают ограничения
  • документация строится поверх DTO, а не отдельно от них

Такой подход снижает расхождение между runtime-валидацией и публичным API-контрактом, но требует строгой дисциплины в описании типов и метаданных