Автогенерация документации по валидации

Метаданные валидации, которые создаются декораторами class-validator, формируют структурированный слой описания данных, пригодный не только для проверки входных значений, но и для автоматической генерации документации. Каждое правило, заданное через декораторы, сохраняется в глобальном хранилище метаданных библиотеки и может быть извлечено во время выполнения приложения.

Основная идея автогенерации документации строится на том, что валидационные декораторы уже содержат достаточный набор информации о структуре данных:

  • тип поля (строка, число, массив)
  • ограничения (минимум, максимум, длина)
  • обязательность поля
  • дополнительные правила (регулярные выражения, кастомные проверки)

Пример модели:

import { IsString, IsInt, Min, Max, Length, IsOptional } from "class-validator";

class CreateUserDto {
  @IsString()
  @Length(3, 30)
  username;

  @IsInt()
  @Min(0)
  @Max(120)
  age;

  @IsOptional()
  @IsString()
  bio;
}

Каждый декоратор добавляет запись в внутреннее хранилище MetadataStorage, которое используется при вызове validate() и может быть использовано для построения документации.

Интроспекция правил валидации

class-validator предоставляет доступ к метаданным через getMetadataStorage():

import { getMetadataStorage } from "class-validator";

const metadataStorage = getMetadataStorage();
const metadata = metadataStorage.getTargetValidationMetadatas(
  CreateUserDto,
  "",
  false,
  false
);

Результат представляет собой массив объектов, описывающих каждое правило:

  • type — тип валидатора (isString, min, max и т.д.)
  • propertyName — имя поля
  • constraints — параметры декоратора
  • validationTypeOptions — дополнительные настройки

Эти данные являются основой для автоматического построения схемы документации.

Построение структуры модели данных

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

Алгоритм включает:

  • группировку метаданных по propertyName
  • определение базового типа поля
  • агрегирование ограничений

Пример структуры:

{
  username: {
    type: "string",
    required: true,
    minLength: 3,
    maxLength: 30
  },
  age: {
    type: "number",
    required: true,
    min: 0,
    max: 120
  }
}

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

  • reflect-metadata (design:type)
  • анализ декораторов
  • соглашения DTO
import "reflect-metadata";

const type = Reflect.getMetadata("design:type", CreateUserDto.prototype, "username");

Генерация JSON Schema

Одним из наиболее распространённых форматов документации является JSON Schema. Преобразование метаданных в JSON Schema позволяет использовать документацию в:

  • Swagger / OpenAPI
  • GraphQL schema generators
  • UI-формы (React Hook Form, JSON Schema Form)

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

function buildJsonSchema(dtoClass) {
  const metadataStorage = getMetadataStorage();
  const metadatas = metadataStorage.getTargetValidationMetadatas(
    dtoClass,
    "",
    false,
    false
  );

  const schema = { type: "object", properties: {}, required: [] };

  for (const meta of metadatas) {
    const prop = meta.propertyName;

    if (!schema.properties[prop]) {
      schema.properties[prop] = {};
    }

    switch (meta.type) {
      case "isString":
        schema.properties[prop].type = "string";
        break;

      case "minLength":
        schema.properties[prop].minLength = meta.constraints[0];
        break;

      case "maxLength":
        schema.properties[prop].maxLength = meta.constraints[0];
        break;

      case "isInt":
        schema.properties[prop].type = "integer";
        break;

      case "min":
        schema.properties[prop].minimum = meta.constraints[0];
        break;

      case "max":
        schema.properties[prop].maximum = meta.constraints[0];
        break;
    }

    if (meta.type === "isDefined") {
      schema.required.push(prop);
    }
  }

  return schema;
}

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

Интеграция с OpenAPI

При использовании class-validator вместе с class-transformer в серверных фреймворках возможно автоматическое построение OpenAPI спецификации.

Типовой подход:

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

Пример результата OpenAPI:

{
  "User": {
    "type": "object",
    "properties": {
      "username": {
        "type": "string",
        "minLength": 3,
        "maxLength": 30
      },
      "age": {
        "type": "integer",
        "minimum": 0,
        "maximum": 120
      }
    },
    "required": ["username", "age"]
  }
}

В экосистемах, подобных NestJS, подобная генерация часто выполняется через дополнительные слои, но базовый источник данных остаётся тем же — метаданные class-validator.

Обработка вложенных объектов

Сложные DTO содержат вложенные структуры:

class Address {
  @IsString()
  city;
}

class User {
  @ValidateNested()
  address;
}

Для генерации документации требуется рекурсивный обход:

  • обнаружение ValidateNested
  • извлечение вложенного класса
  • построение под-схемы
function processNested(dtoClass) {
  const schema = buildJsonSchema(dtoClass);

  const metadataStorage = getMetadataStorage();
  const nested = metadataStorage.getTargetValidationMetadatas(
    dtoClass,
    "",
    false,
    false
  ).filter(m => m.type === "validateNested");

  for (const n of nested) {
    const childType = Reflect.getMetadata("design:type", dtoClass.prototype, n.propertyName);
    schema.properties[n.propertyName] = processNested(childType);
  }

  return schema;
}

Кастомные валидаторы и их документирование

Кастомные декораторы создаются через registerDecorator:

import { registerDecorator } from "class-validator";

function IsEven() {
  return function (object, propertyName) {
    registerDecorator({
      name: "isEven",
      target: object.constructor,
      propertyName,
      validator: {
        validate(value) {
          return value % 2 === 0;
        }
      }
    });
  };
}

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

Расширенный подход:

  • регистрация метаданных вручную
  • добавление описаний валидаторов
  • использование словаря правил
const validatorDocs = {
  isEven: {
    type: "number",
    description: "Чётное число"
  }
};

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

Объединение нескольких источников метаданных

В реальных приложениях документация формируется не только из class-validator, но и из:

  • class-transformer (трансформация типов)
  • reflect-metadata (типы)
  • пользовательских аннотаций

Итоговая модель строится как объединение слоёв:

  1. базовый тип (design:type)
  2. ограничения валидации
  3. дополнительные описания
  4. бизнес-метаданные

При конфликте правил приоритет обычно отдаётся наиболее строгим ограничениям (например, минимальная длина из нескольких источников).

Оптимизация генерации

При большом количестве DTO генерация документации может стать затратной операцией. Оптимизация достигается через:

  • кэширование результатов getMetadataStorage()
  • предварительную индексацию по классу
  • ленивую генерацию схем
const schemaCache = new Map();

function getSchema(dtoClass) {
  if (schemaCache.has(dtoClass)) {
    return schemaCache.get(dtoClass);
  }

  const schema = buildJsonSchema(dtoClass);
  schemaCache.set(dtoClass, schema);

  return schema;
}

Ограничения подхода

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

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

Несмотря на это, подход остаётся практичным в системах, где DTO уже являются центральной моделью данных, а валидация определяет контракт API.