Миграция с других библиотек валидации

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

Ключевая особенность архитектуры заключается в том, что данные представляются как экземпляры классов, а не как произвольные объекты. Это определяет основной принцип миграции: переход от схем и функций к структурированным DTO-моделям.

Основные различия подходов:

  • Joi / Yup — схема как отдельный объект
  • AJV — JSON Schema как источник истины
  • express-validator — цепочки middleware и ручная проверка request
  • Zod — функционально-типизированные схемы
  • class-validator — классы с декларативными декораторами

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


Перенос концепции схемы в классы DTO

В большинстве библиотек валидации схема отделена от данных. В class-validator схема становится частью структуры данных.

Пример базовой модели:

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

class UserDto {
  @IsString()
  name: string;

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

Эквивалент в Joi:

const schema = Joi.object({
  name: Joi.string().required(),
  age: Joi.number().integer().min(0)
});

Ключевое изменение при миграции заключается в том, что описание структуры переносится из внешнего объекта в типизированный класс.


Миграция с Joi

Сопоставление типов

Joi class-validator
Joi.string() @IsString()
Joi.number() @IsNumber()
Joi.boolean() @IsBoolean()
Joi.array() @IsArray()
Joi.object() вложенный класс

Обязательность значений

В Joi:

Joi.string().required()

В class-validator:

import { IsDefined, IsString } from "class-validator";

class ExampleDto {
  @IsDefined()
  @IsString()
  value: string;
}

Или через @IsOptional() для обратной логики.


Вложенные структуры

Joi:

Joi.object({
  user: Joi.object({
    name: Joi.string()
  })
});

class-validator:

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

class User {
  @IsString()
  name: string;
}

class RootDto {
  @ValidateNested()
  @Type(() => User)
  user: User;
}

Здесь добавляется важная зависимость: class-transformer, обеспечивающая корректную десериализацию вложенных объектов.


Массивы

Joi:

Joi.array().items(Joi.string())

class-validator:

import { IsArray, IsString } from "class-validator";

class ArrayDto {
  @IsArray()
  @IsString({ each: true })
  items: string[];
}

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


Миграция с Yup

Yup использует цепочки методов и императивно-декларативный стиль.

Базовые типы

Yup:

Yup.string().required().min(3)

class-validator:

import { IsString, MinLength } from "class-validator";

class Dto {
  @IsString()
  @MinLength(3)
  value: string;
}

Nullable и optional

Yup:

Yup.string().nullable().optional()

class-validator:

import { IsOptional, IsString } from "class-validator";

class Dto {
  @IsOptional()
  @IsString()
  value?: string;
}

Разделение optional и nullable в class-validator требует отдельного понимания:

  • @IsOptional() — значение может отсутствовать
  • @IsDefined() — значение обязательно
  • null требует дополнительных кастомных валидаторов

Кастомная логика

Yup:

Yup.string().test("check", "error", value => value === "ok")

class-validator:

import { registerDecorator, ValidationArguments } from "class-validator";

function IsOk() {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "IsOk",
      target: object.constructor,
      propertyName,
      validator: {
        validate(value: any) {
          return value === "ok";
        },
        defaultMessage(args: ValidationArguments) {
          return "value must be ok";
        }
      }
    });
  };
}

Миграция с express-validator

express-validator работает через middleware и цепочки проверок запроса.

Пример express-validator

check("email").isEmail().notEmpty()

Перенос в class-validator

import { IsEmail, IsNotEmpty } from "class-validator";

class LoginDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;
}

Ключевое отличие архитектуры

express-validator:

  • проверка происходит в middleware
  • данные остаются частью req

class-validator:

  • проверка происходит над DTO
  • используется явное преобразование входных данных

Обработка request

express-validator:

app.post("/login", [
  check("email").isEmail()
], handler);

class-validator (типичный паттерн):

const dto = plainToInstance(LoginDto, req.body);
const errors = await validate(dto);

Сдвиг происходит от middleware-ориентированной модели к объектной модели данных.


Миграция с AJV (JSON Schema)

AJV основан на JSON Schema, где структура описывается декларативным JSON-объектом.

Пример JSON Schema

{
  "type": "object",
  "properties": {
    "age": { "type": "integer", "minimum": 0 }
  }
}

class-validator эквивалент

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

class Dto {
  @IsInt()
  @Min(0)
  age: number;
}

Ключевое отличие

AJV:

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

class-validator:

  • схема связана с TypeScript классами
  • ориентирован на backend-архитектуры с DI и DTO

Работа с преобразованием данных

Важный аспект миграции — преобразование типов.

В большинстве библиотек входные данные остаются «сырыми». В class-validator используется дополнительный слой:

  • class-transformer
  • plainToInstance()

Пример:

import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";

const dto = plainToInstance(UserDto, request.body);
const result = await validate(dto);

Без преобразования декораторы могут работать некорректно, особенно для чисел и вложенных объектов.


Типовые расхождения при миграции

Строка vs число

Входные данные HTTP часто приходят как строки.

Joi и Yup автоматически приводят типы при включённом coercion.

class-validator этого не делает по умолчанию.

Решение:

import { Type } from "class-transformer";
import { IsInt } from "class-validator";

class Dto {
  @Type(() => Number)
  @IsInt()
  age: number;
}

Валидация пустых значений

Разные библиотеки по-разному трактуют:

  • ""
  • null
  • undefined

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

  • @IsOptional()
  • @IsNotEmpty()
  • кастомные валидаторы

Миграция сложных правил

Условная валидация

В Joi:

Joi.when("role", {
  is: "admin",
  then: Joi.string().required()
})

В class-validator используется кастомная логика:

import { ValidateIf, IsString } from "class-validator";

class Dto {
  role: string;

  @ValidateIf(o => o.role === "admin")
  @IsString()
  adminCode: string;
}

Зависимости между полями

class-validator реализует межполевая проверка через:

  • @ValidateIf
  • кастомные валидаторы на уровне объекта
import { ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";

@ValidatorConstraint({ name: "match", async: false })
class MatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: any) {
    const [relatedProperty] = args.constraints;
    return value === args.object[relatedProperty];
  }
}

Организация DTO после миграции

При переходе с функциональных схем структура проекта изменяется:

  • схемы → классы DTO
  • валидаторы → декораторы
  • middleware → pipeline трансформации и валидации

Типовая структура:

  • dto/
  • validators/
  • pipes/ (в связке с NestJS-подобной архитектурой)

Ошибки, возникающие при миграции

Потеря преобразования типов

Без class-transformer числовые и вложенные значения остаются строками.

Отсутствие each для массивов

Проверка массива без each приводит к валидации только контейнера.

Игнорирование undefined vs null

Поведение отличается от Joi/Yup и требует явной настройки.

Неинициализированные классы

Передача plain-object без plainToInstance приводит к пропуску декораторов.


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

Подход Модель
Joi / Yup схема как функция
AJV JSON Schema
express-validator middleware chain
class-validator объектно-декларативная модель

Смена библиотеки означает не только замену API, но и перестройку слоя обработки входных данных.