Генерация OpenAPI спецификаций

OpenAPI описывает контракт HTTP API через формализованную схему, включающую структуры запросов, ответов и параметров. Центральной частью становится описание моделей данных (schemas), которые определяют форму объектов, передаваемых между клиентом и сервером. В JavaScript-экосистеме при использовании TypeScript эти схемы часто выводятся из классов DTO, где одновременно присутствуют декларации типов и правила валидации.

Библиотека class-validator задаёт декларативный слой ограничений на уровне классов, однако сама по себе не генерирует OpenAPI. Генерация происходит через промежуточные инструменты, которые читают метаданные декораторов и преобразуют их в JSON Schema (компоненты OpenAPI).


Метаданные декораторов как источник схемы

class-validator опирается на декораторы, которые сохраняются в runtime-метаданных через reflect-metadata. Эти данные включают:

  • тип валидируемого поля (строка, число, массив)
  • ограничения (min, max, длина, регулярные выражения)
  • составные правила (nested validation, массив объектов)
  • группы валидации

Пример 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.


Преобразование class-validator в JSON Schema

Генерация 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"]
}

Роль reflect-metadata и design:type

TypeScript-компилятор при включённой опции emitDecoratorMetadata добавляет информацию о типах:

design:type → String / Number / Boolean / Array

Эти данные используются как базовый слой, поверх которого накладываются ограничения class-validator.

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


Инструменты генерации OpenAPI из class-validator

routing-controllers-openapi

Библиотека routing-controllers-openapi анализирует контроллеры и DTO, извлекая:

  • параметры маршрутов
  • body schema
  • query параметры
  • validation decorators

Пример интеграции:

import { createExpressServer } from "routing-controllers";
import { getMetadataArgsStorage } from "routing-controllers";
import { routingControllersToSpec } from "routing-controllers-openapi";

const spec = routingControllersToSpec(
  getMetadataArgsStorage(),
  {},
  {
    components: {
      schemas: {}
    }
  }
);

tsoa (TypeScript OpenAPI generator)

tsoa строит OpenAPI на основе TypeScript-классов и аннотаций.

class-validator используется как дополнительный слой ограничений:

export class UserController {
  public async createUser(@Body() body: CreateUserDto): Promise<User> {
    return new User();
  }
}

tsoa извлекает тип DTO и строит schema, затем могут учитываться валидаторы через расширения.


NestJS Swagger (@nestjs/swagger)

Наиболее распространённый вариант интеграции 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, который:

  • читает metadata reflect
  • анализирует class-validator
  • комбинирует с @ApiProperty
  • строит OpenAPI schema

Автоматическое извлечение ограничений

При генерации схемы важно учитывать пересечение двух источников:

  1. TypeScript типы (design:type)
  2. class-validator декораторы

Приоритет обычно отдаётся декораторам, так как они содержат более точные ограничения.

Пример конфликта:

@IsString()
age: number;

TypeScript указывает number, но валидатор требует string. Генератор OpenAPI может:

  • либо выбрать валидатор как источник истины
  • либо выбросить предупреждение о несоответствии

Сложные структуры DTO

Вложенные объекты

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"
  }
}

Enum и ограниченные множества значений

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"]
}

Optional поля и nullable-семантика

@IsOptional()
@IsString()
nickname?: string;

Генерация учитывает:

  • отсутствие поля в required
  • возможное значение undefined

В OpenAPI это отражается как:

{
  "type": "string",
  "nullable": true
}

(в зависимости от версии спецификации и генератора)


Условия групп валидации и их влияние на схемы

class-validator поддерживает groups:

@IsString({ groups: ["create"] })
@IsString({ groups: ["update"] })
name: string;

Генерация OpenAPI сталкивается с проблемой:

  • одна схема не может выразить разные режимы валидации
  • требуется либо дублирование схем (CreateUserDto / UpdateUserDto)
  • либо расширение через oneOf

Наследование DTO и композиция схем

export class BaseUserDto {
  @IsString()
  username: string;
}

export class ExtendedUserDto extends BaseUserDto {
  @IsInt()
  age: number;
}

В OpenAPI это может отображаться как:

  • объединённая схема (allOf)
  • либо inline расширение
{
  "allOf": [
    { "$ref": "#/components/schemas/BaseUserDto" },
    {
      "type": "object",
      "properties": {
        "age": { "type": "integer" }
      }
    }
  ]
}

Ограничения генерации из class-validator

1. Потеря семантики runtime-валидации

Некоторые валидаторы не имеют прямого аналога в JSON Schema:

  • кастомные ValidatorConstraint
  • асинхронные проверки (database checks)
  • сложные cross-field зависимости

2. Условная логика

if (type === "A") {
  // поле обязательно
}

OpenAPI не поддерживает динамическую валидацию без расширений (oneOf, anyOf).


3. Частичная несовместимость типов

  • @IsNumberString() → string + numeric constraint (нет прямого аналога)
  • @IsPhoneNumber() → pattern approximation
  • кастомные regex могут быть неточными

Построение единого пайплайна генерации

Типичная цепочка:

  1. DTO-класс
  2. class-validator decorators → constraints
  3. reflect-metadata → type metadata
  4. transformer (routing-controllers / NestJS / tsoa)
  5. JSON Schema builder
  6. OpenAPI Document Assembly

Расширение схем через пользовательские метаданные

Для повышения точности используются дополнительные декораторы:

@ApiProperty({
  description: "Имя пользователя",
  example: "john_doe"
})

Такие аннотации дополняют данные class-validator, формируя полноценную OpenAPI модель, включающую:

  • описание полей
  • примеры
  • форматирование
  • deprecated флаги

Влияние class-transformer на OpenAPI

class-transformer участвует в генерации схем, обеспечивая:

  • преобразование типов (string → number)
  • вложенные объекты
  • контроль сериализации
@Type(() => Number)
@IsNumber()
value: number;

Генератор использует @Type как дополнительный источник информации о runtime-типе.


Итоговая модель взаимодействия

OpenAPI-генерация на основе class-validator представляет собой многослойный процесс, где:

  • class-validator задаёт ограничения
  • TypeScript даёт базовые типы
  • reflect-metadata предоставляет runtime-информацию
  • внешние инструменты формируют JSON Schema
  • Swagger/OpenAPI объединяет всё в спецификацию

Эта модель позволяет поддерживать синхронизацию между валидацией данных и контрактом API без дублирования описаний, при условии строгого соответствия между декораторами и типами данных.