OpenAPI описывает контракт HTTP API через формализованную схему, включающую структуры запросов, ответов и параметров. Центральной частью становится описание моделей данных (schemas), которые определяют форму объектов, передаваемых между клиентом и сервером. В JavaScript-экосистеме при использовании TypeScript эти схемы часто выводятся из классов DTO, где одновременно присутствуют декларации типов и правила валидации.
Библиотека class-validator задаёт декларативный слой ограничений на уровне классов, однако сама по себе не генерирует OpenAPI. Генерация происходит через промежуточные инструменты, которые читают метаданные декораторов и преобразуют их в JSON Schema (компоненты OpenAPI).
class-validator опирается на декораторы, которые сохраняются в runtime-метаданных через reflect-metadata. Эти данные включают:
Пример DTO:
import { IsString, IsInt, MinLength, MaxLength, IsOptional } from "class-validator";
export class CreateUserDto {
@IsString()
@MinLength(3)
@MaxLength(30)
username: string;
@IsInt()
age: number;
@IsOptional()
@IsString()
bio?: string;
}
Эти декораторы формируют метаданные, которые могут быть интерпретированы генератором OpenAPI.
Генерация OpenAPI основана на отображении валидаторов в ограничения JSON Schema.
@IsString() → "type": "string"@IsInt() → "type": "integer"@IsBoolean() → "type": "boolean"@IsArray() → "type": "array"@MinLength(n) → "minLength": n@MaxLength(n) → "maxLength": n@Min(n) → "minimum": n@Max(n) → "maximum": n@Matches(regex) → "pattern": "..."Пример результирующей схемы:
{
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 30
},
"age": {
"type": "integer"
},
"bio": {
"type": "string"
}
},
"required": ["username", "age"]
}
TypeScript-компилятор при включённой опции
emitDecoratorMetadata добавляет информацию о типах:
design:type → String / Number / Boolean / Array
Эти данные используются как базовый слой, поверх которого накладываются ограничения class-validator.
Без этой метаинформации генерация OpenAPI становится неполной, поскольку отсутствует исходный тип поля.
Библиотека routing-controllers-openapi анализирует
контроллеры и DTO, извлекая:
Пример интеграции:
import { createExpressServer } from "routing-controllers";
import { getMetadataArgsStorage } from "routing-controllers";
import { routingControllersToSpec } from "routing-controllers-openapi";
const spec = routingControllersToSpec(
getMetadataArgsStorage(),
{},
{
components: {
schemas: {}
}
}
);
tsoa строит OpenAPI на основе TypeScript-классов и
аннотаций.
class-validator используется как дополнительный слой ограничений:
export class UserController {
public async createUser(@Body() body: CreateUserDto): Promise<User> {
return new User();
}
}
tsoa извлекает тип DTO и строит schema, затем могут учитываться валидаторы через расширения.
Наиболее распространённый вариант интеграции class-validator с OpenAPI.
import { ApiProperty } from "@nestjs/swagger";
import { IsString, MinLength } from "class-validator";
export class CreateUserDto {
@ApiProperty({ minLength: 3, maxLength: 30 })
@IsString()
@MinLength(3)
username: string;
}
В NestJS генерация происходит через
SwaggerModule.createDocument, который:
При генерации схемы важно учитывать пересечение двух источников:
Приоритет обычно отдаётся декораторам, так как они содержат более точные ограничения.
Пример конфликта:
@IsString()
age: number;
TypeScript указывает number, но валидатор требует
string. Генератор OpenAPI может:
export class AddressDto {
@IsString()
city: string;
}
export class UserDto {
@ValidateNested()
address: AddressDto;
}
OpenAPI преобразует это в ссылочную схему:
{
"properties": {
"address": {
"$ref": "#/components/schemas/AddressDto"
}
}
}
import { Type } from "class-transformer";
import { ValidateNested, IsArray } from "class-validator";
export class UserDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => AddressDto)
addresses: AddressDto[];
}
Результирующая схема:
{
"type": "array",
"items": {
"$ref": "#/components/schemas/AddressDto"
}
}
import { IsEnum } from "class-validator";
export enum Role {
ADMIN = "admin",
USER = "user"
}
export class UserDto {
@IsEnum(Role)
role: Role;
}
OpenAPI:
{
"type": "string",
"enum": ["admin", "user"]
}
@IsOptional()
@IsString()
nickname?: string;
Генерация учитывает:
В OpenAPI это отражается как:
{
"type": "string",
"nullable": true
}
(в зависимости от версии спецификации и генератора)
class-validator поддерживает groups:
@IsString({ groups: ["create"] })
@IsString({ groups: ["update"] })
name: string;
Генерация OpenAPI сталкивается с проблемой:
oneOfexport class BaseUserDto {
@IsString()
username: string;
}
export class ExtendedUserDto extends BaseUserDto {
@IsInt()
age: number;
}
В OpenAPI это может отображаться как:
{
"allOf": [
{ "$ref": "#/components/schemas/BaseUserDto" },
{
"type": "object",
"properties": {
"age": { "type": "integer" }
}
}
]
}
Некоторые валидаторы не имеют прямого аналога в JSON Schema:
ValidatorConstraintif (type === "A") {
// поле обязательно
}
OpenAPI не поддерживает динамическую валидацию без расширений
(oneOf, anyOf).
@IsNumberString() → string + numeric constraint (нет
прямого аналога)@IsPhoneNumber() → pattern approximationТипичная цепочка:
Для повышения точности используются дополнительные декораторы:
@ApiProperty({
description: "Имя пользователя",
example: "john_doe"
})
Такие аннотации дополняют данные class-validator, формируя полноценную OpenAPI модель, включающую:
class-transformer участвует в генерации схем, обеспечивая:
@Type(() => Number)
@IsNumber()
value: number;
Генератор использует @Type как дополнительный источник
информации о runtime-типе.
OpenAPI-генерация на основе class-validator представляет собой многослойный процесс, где:
Эта модель позволяет поддерживать синхронизацию между валидацией данных и контрактом API без дублирования описаний, при условии строгого соответствия между декораторами и типами данных.