Автоматическая генерация документации 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 сам по себе не генерирует OpenAPI-схемы.
Он хранит метаданные, которые:
reflect-metadataПоэтому автоматическая документация почти всегда требует дополнительного слоя, который интерпретирует эти метаданные.
В экосистеме 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,
потому что не все ограничения однозначно мапятся.
Автоматическая документация в TypeScript-проектах зависит от включённого параметра:
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
Без этого метаданные типов недоступны, и генерация схем становится неполной.
Пример, где тип извлекается автоматически:
class UserDto {
@IsString()
name: string;
}
Тип string может быть извлечён из metadata, но только
для базовых случаев.
Наибольшие сложности возникают при вложенных структурах:
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[];
}
Для документации это означает:
AddressDtoSwagger-интеграции должны явно знать тип через @Type()
или @ApiProperty({ type: ... }), иначе схема становится
any.
Типичная система генерации документации строит соответствия между декораторами и JSON Schema.
@IsString() → type: string@Length(min, max) → minLength,
maxLength@Matches(regex) → pattern@IsInt() → type: integer@Min() → minimum@Max() → maximum@IsBoolean() → type: boolean@IsEmail() → format: email@IsUUID() → format: uuid@IsArray() → type: array@ArrayMinSize() → minItems@ArrayMaxSize() → maxItemsimport { IsEnum } from 'class-validator';
enum Role {
Admin = 'admin',
User = 'user',
}
class UserDto {
@IsEnum(Role)
role: Role;
}
В OpenAPI это становится:
type: stringenum: ['admin', 'user']NestJS позволяет комбинировать автоматическую и ручную документацию:
@ApiProperty({
description: 'Имя пользователя',
minLength: 2,
maxLength: 50,
})
@IsString()
@Length(2, 50)
name: string;
В этом случае class-validator и
@ApiProperty дублируют информацию, но служат разным
целям:
Полная автоматизация сталкивается с ограничениями:
Пример кастомного валидатора:
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
@ValidatorConstraint()
class IsOddConstraint implements ValidatorConstraintInterface {
validate(value: number) {
return value % 2 === 1;
}
}
Такое правило невозможно напрямую выразить в JSON Schema без расширений.
Более тесная интеграция достигается в
routing-controllers и
routing-controllers-openapi.
import { JsonController, Post, Body } from 'routing-controllers';
@JsonController()
class UserController {
@Post('/users')
create(@Body() body: CreateUserDto) {
return body;
}
}
При включении OpenAPI-генерации:
class-validator используется как источник
ограничений@ApiPropertytsoa использует другой подход:
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В системах, использующих class-validator, стабильная
документация достигается только при соблюдении архитектурного
принципа:
Такой подход снижает расхождение между runtime-валидацией и публичным API-контрактом, но требует строгой дисциплины в описании типов и метаданных