Генерация JSON Schema из классов

JSON Schema представляет собой декларативное описание структуры данных, используемое для валидации, генерации документации и описания контрактов API. При работе с классами в JavaScript/TypeScript возникает задача преобразования объектной модели, основанной на классах и декораторах, в формализованную JSON Schema. В контексте экосистемы class-validator это достигается через анализ метаданных декораторов и последующую трансформацию в спецификацию JSON Schema.


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

Ключевые типы ограничений:

  • Ограничения типов: @IsString(), @IsNumber(), @IsBoolean()
  • Структурные ограничения: @IsArray(), @ValidateNested()
  • Логические ограничения: @IsOptional(), @IsDefined()
  • Ограничения значений: @Min(), @Max(), @Length(), @Matches()
  • Перечисления: @IsEnum()

Каждое из этих ограничений потенциально может быть отображено в JSON Schema как комбинация полей type, enum, pattern, minimum, maximum, items, required.


Метаданные TypeScript и роль reflect-metadata

Генерация схемы невозможна без включённой поддержки метаданных TypeScript:

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

При включённой опции emitDecoratorMetadata компилятор добавляет информацию о типах свойств, доступную через Reflect.getMetadata. Это позволяет определить базовый тип поля даже без явного декоратора class-validator.

Пример извлечения типа:

Reflect.getMetadata("design:type", User.prototype, "age");

Полученное значение используется как отправная точка для построения JSON Schema, которая затем уточняется декораторами.


Базовое преобразование классов в JSON Schema

Типичный класс с валидацией:

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

class User {
  @IsString()
  name: string;

  @IsInt()
  @Min(0)
  @Max(120)
  age: number;
}

Соответствующая JSON Schema:

{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer", "minimum": 0, "maximum": 120 }
  },
  "required": ["name", "age"]
}

Логика трансформации:

  • @IsString()"type": "string"
  • @IsInt()"type": "integer"
  • @Min(n)"minimum": n
  • @Max(n)"maximum": n
  • отсутствие @IsOptional() → добавление в required

Использование class-validator-jsonschema

На практике генерация JSON Schema выполняется через библиотеку class-validator-jsonschema, которая агрегирует метаданные class-validator и class-transformer.

Установка:

npm install class-validator class-transformer class-validator-jsonschema reflect-metadata

Генерация схемы:

import "reflect-metadata";
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
import { getMetadataStorage } from "class-validator";

const schemas = validationMetadatasToSchemas({
  classValidatorMetadataStorage: getMetadataStorage()
});

Результат содержит JSON Schema для всех зарегистрированных классов.


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

При использовании @ValidateNested() возникает необходимость рекурсивной обработки вложенных объектов.

import { ValidateNested, IsString } from "class-validator";
import { Type } from "class-transformer";

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

class User {
  @ValidateNested()
  @Type(() => Address)
  address: Address;
}

JSON Schema:

{
  "type": "object",
  "properties": {
    "address": {
      "$ref": "#/definitions/Address"
    }
  }
}

Дополнительно формируется секция definitions, содержащая описание Address.

Механизм работы:

  • @ValidateNested() сигнализирует о вложенной структуре
  • @Type(() => Class) позволяет определить конкретный класс для построения схемы
  • генератор добавляет ссылку $ref вместо дублирования структуры

Обработка массивов

Массивы требуют комбинации @IsArray() и вложенной валидации элементов.

class User {
  @IsString({ each: true })
  tags: string[];
}

JSON Schema:

{
  "type": "object",
  "properties": {
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}

Ключевые преобразования:

  • @IsArray()"type": "array"
  • each: true → применение ограничения к items

Перечисления и фиксированные значения

@IsEnum() преобразуется в JSON Schema через enum.

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

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

JSON Schema:

{
  "type": "object",
  "properties": {
    "role": {
      "enum": ["admin", "user"]
    }
  }
}

Если enum числовой, сохраняется соответствующая числовая схема без преобразования типов.


Условные поля и optional-логика

@IsOptional() влияет на секцию required. Поле исключается из списка обязательных свойств.

class User {
  @IsOptional()
  @IsString()
  nickname?: string;
}

JSON Schema:

{
  "type": "object",
  "properties": {
    "nickname": { "type": "string" }
  }
}

Отсутствие required означает допустимость undefined.


Ограничения строковых и числовых значений

Строковые ограничения:

  • @Length(min, max)minLength, maxLength
  • @Matches(regex)pattern
class User {
  @Matches(/^[a-z]+$/)
  username: string;
}

JSON Schema:

{
  "username": {
    "type": "string",
    "pattern": "^[a-z]+$"
  }
}

Числовые ограничения:

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

Композиция нескольких декораторов

При наличии нескольких декораторов формируется пересечение ограничений. Порядок применения декораторов не влияет на результат, так как метаданные агрегируются.

Пример:

class Product {
  @IsString()
  @Length(3, 20)
  title: string;
}

Результат:

{
  "title": {
    "type": "string",
    "minLength": 3,
    "maxLength": 20
  }
}

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

class-validator позволяет создавать кастомные декораторы через registerDecorator. Такие ограничения не имеют прямого соответствия в JSON Schema и требуют ручного маппинга.

import { registerDecorator } from "class-validator";

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

JSON Schema не содержит стандартного аналога, поэтому возможные стратегии:

  • игнорирование при генерации
  • добавление расширений через x- поля
  • использование pattern или modulo (не стандарт JSON Schema)

Пример расширения:

{
  "x-isEven": true
}

Расхождения между class-validator и JSON Schema

Несмотря на схожесть целей, модели различаются:

  • class-validator ориентирован на runtime-валидацию
  • JSON Schema ориентирован на декларативное описание данных

Ключевые расхождения:

  • отсутствует прямой аналог сложных кастомных валидаторов
  • частичная поддержка условной логики
  • ограниченная выразительность для сложных бизнес-правил
  • различие в трактовке undefined и null

Дополнительно JSON Schema требует явного указания nullable (в зависимости от версии спецификации), тогда как class-validator работает с undefined через @IsOptional().


Опции генерации и контроль результата

Библиотеки генерации обычно предоставляют настройки:

  • включение/исключение required
  • генерация definitions или inline-схем
  • обработка неизвестных типов
  • префиксы для расширений x-

Пример конфигурации:

validationMetadatasToSchemas({
  classValidatorMetadataStorage: getMetadataStorage(),
  refPointerPrefix: "#/definitions/"
});

Типизация и интеграция с TypeScript

Результирующая JSON Schema может быть типизирована:

type JSONSchema = {
  type?: string;
  properties?: Record<string, JSONSchema>;
  required?: string[];
};

Это позволяет использовать схему для:

  • генерации API-контрактов
  • валидации входных данных вне runtime class-validator
  • автогенерации документации

Производственные сценарии использования

При масштабных проектах схема используется как единый источник правды для:

  • валидации REST-запросов
  • описания DTO
  • генерации OpenAPI (через промежуточную JSON Schema)
  • межсервисной коммуникации

Связка class-validator + JSON Schema обеспечивает переход от императивной валидации к декларативному контракту данных, где классы выступают первичной моделью, а схема — производным артефактом.