Валидация на уровне класса

Модель данных и роль классов в системе валидации

При построении структурированных приложений на JavaScript и TypeScript классы часто выступают в роли контейнеров данных, описывающих доменные сущности: пользователь, заказ, продукт, транзакция. В такой модели входящие данные приводятся к экземплярам классов, после чего применяется проверка корректности структуры и значений.

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

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


Архитектура class-validator и механизм работы декораторов

В основе библиотеки лежит система метаданных. Каждый декоратор записывает информацию о правилах проверки в реестр, связанный с целевым классом. При вызове функции валидации эта метаинформация извлекается и применяется к экземпляру объекта.

Основные элементы архитектуры:

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

Валидация запускается через функции:

  • validate(instance)
  • validateOrReject(instance)
  • validateSync(instance)

Каждая из них анализирует объект и возвращает список ошибок или выбрасывает исключение.


Отличие валидации свойств и валидации класса

Валидация свойств применяется к отдельным полям:

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

class User {
  @IsString()
  name;

  @IsEmail()
  email;
}

Такая модель подходит для локальных ограничений.

Однако существуют сценарии, где изолированная проверка полей недостаточна. Например:

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

В этих случаях применяется класс-уровневая валидация.


Класс-уровневые ограничения через ValidatorConstraint

Для реализации логики на уровне объекта используется ValidatorConstraint.

Базовая структура:

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from "class-validator";

Создание ограничения:

@ValidatorConstraint({ name: "matchPasswords", async: false })
class MatchPasswordsConstraint {
  validate(object) {
    return object.password === object.confirmPassword;
  }

  defaultMessage() {
    return "Пароли не совпадают";
  }
}

Применение через декоратор:

import { Validate } from "class-validator";

class RegisterUser {
  password;

  confirmPassword;

  @Validate(MatchPasswordsConstraint)
  static passwordCheck;
}

В данном случае логика работает на уровне всего объекта, а не отдельных свойств.


Контекст ValidationArguments

Более гибкая проверка возможна через аргументы валидатора:

class MatchFieldsConstraint {
  validate(value, args) {
    const object = args.object;
    return object.password === object.confirmPassword;
  }

  defaultMessage() {
    return "Поля не совпадают";
  }
}

ValidationArguments предоставляет:

  • object — текущий экземпляр класса
  • value — значение текущего поля (если применимо)
  • constraints — дополнительные параметры декоратора
  • property — имя свойства

Это позволяет реализовывать сложные межполевые зависимости.


Регистрация пользовательских декораторов

Вместо использования класса напрямую создаётся кастомный декоратор:

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

function Match(property, options) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options,
      constraints: [property],
      validator: {
        validate(value, args) {
          const [relatedProperty] = args.constraints;
          const relatedValue = args.object[relatedProperty];
          return value === relatedValue;
        },
      },
    });
  };
}

Использование:

class User {
  password;

  @Match("password")
  confirmPassword;
}

Такой подход переносит валидацию ближе к декларативной модели, сохраняя при этом логику на уровне класса.


Межполевые зависимости и согласованность состояния

Класс-уровневая валидация часто применяется для обеспечения целостности состояния объекта.

Пример: диапазон дат

@ValidatorConstraint({ name: "dateRange" })
class DateRangeConstraint {
  validate(_, args) {
    const obj = args.object;
    return new Date(obj.startDate) < new Date(obj.endDate);
  }
}
class Event {
  startDate;

  endDate;
}

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


Асинхронная валидация на уровне класса

Некоторые проверки требуют внешних данных: базы данных, API, кэша.

@ValidatorConstraint({ async: true })
class UniqueEmailConstraint {
  async validate(email) {
    const user = await database.findUserByEmail(email);
    return !user;
  }

  defaultMessage() {
    return "Email уже существует";
  }
}

Асинхронная логика полностью поддерживается механизмом class-validator, включая цепочку промисов при вызове validate().


Валидация вложенных объектов

Класс-уровневая логика часто комбинируется с вложенными структурами:

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

class Address {
  city;
  street;
}

class User {
  name;

  @ValidateNested()
  @Type(() => Address)
  address;
}

В этом случае каждый вложенный объект проходит собственный цикл валидации, но итоговая структура оценивается как единое целое.


Группы валидации и условные правила

Группы позволяют активировать разные наборы ограничений в зависимости от сценария:

import { IsOptional } from "class-validator";

class User {
  @IsOptional({ groups: ["update"] })
  password;
}

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


Ошибки валидации и структура результата

Результат валидации представляет собой массив объектов:

  • property — имя поля
  • constraints — список нарушенных правил
  • children — вложенные ошибки
  • target — исходный объект

Класс-уровневые ошибки могут быть привязаны к виртуальному свойству или к корневому объекту, формируя глобальное сообщение об ошибке.


Комбинация property-level и class-level проверок

Практическая модель почти всегда сочетает оба подхода:

  • property-level: формат, тип, длина
  • class-level: бизнес-правила и зависимости
class Payment {
  amount;

  currency;

  @Validate(PaymentConsistencyConstraint)
  static _;
}

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


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

Механизм class-validator проходит по метаданным один раз за цикл валидации, после чего последовательно выполняет все проверки.

Класс-уровневые ограничения могут быть более затратными, поскольку:

  • требуют доступа ко всему объекту
  • могут включать асинхронные операции
  • часто выполняют дополнительные вычисления

Оптимизация достигается через:

  • минимизацию количества асинхронных проверок
  • кэширование результатов внешних запросов
  • разделение критичных и второстепенных правил

Типичные сценарии применения

Класс-уровневая валидация используется в доменных моделях, где важна целостность состояния:

  • регистрация пользователей (пароль / подтверждение)
  • финансовые операции (баланс / лимиты)
  • формы с зависимыми полями
  • конфигурационные объекты с взаимными ограничениями
  • API DTO с бизнес-правилами

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