Доступ к объекту валидации внутри декоратора

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

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


Контекст выполнения валидатора

При запуске валидации библиотека формирует структуру, содержащую информацию о:

  • текущем значении свойства;
  • объекте, которому принадлежит свойство;
  • имени свойства;
  • дополнительных параметрах декоратора;
  • метаданных валидации.

Эта структура передаётся в кастомные валидаторы через интерфейс ValidationArguments.


Объект ValidationArguments

Основной способ доступа к данным контекста — использование объекта ValidationArguments, который передаётся в метод validate.

Он содержит следующие ключевые поля:

  • value — значение текущего валидируемого свойства;
  • object — полный объект, содержащий это свойство;
  • property — имя свойства;
  • targetName — имя класса, к которому относится объект;
  • constraints — массив параметров, переданных в декоратор;
  • metadata — дополнительная служебная информация.

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


Доступ к полному объекту внутри кастомного валидатора

Кастомный валидатор в class-validator обычно реализуется через класс, реализующий интерфейс ValidatorConstraintInterface.

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

@ValidatorConstraint({ name: "CustomRule", async: false })
export class CustomRule implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const fullObject = args.object;
    const propertyName = args.property;

    return true;
  }

  defaultMessage(args: ValidationArguments) {
    return "Ошибка валидации";
  }
}

Поле args.object содержит исходный объект, переданный в validate() на верхнем уровне.


Доступ к соседним полям объекта

Наиболее частое применение доступа к объекту — проверка зависимых свойств.

Например, проверка совпадения пароля и подтверждения пароля:

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

@ValidatorConstraint({ name: "MatchPassword", async: false })
export class MatchPassword implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const obj = args.object as any;

    return value === obj.password;
  }

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

В этом случае:

  • value — значение поля подтверждения;
  • obj.password — доступ к другому полю того же объекта.

Использование registerDecorator и передача контекста

Функция registerDecorator позволяет создавать декораторы без явного класса валидатора.

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

export function IsGreaterThan(property: string, options?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "IsGreaterThan",
      target: object.constructor,
      propertyName,
      constraints: [property],
      options,
      validator: {
        validate(value: any, args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          const relatedValue = (args.object as any)[relatedPropertyName];

          return typeof value === "number" &&
                 typeof relatedValue === "number" &&
                 value > relatedValue;
        },
      },
    });
  };
}

Здесь ключевой момент — использование args.constraints для передачи имени сравниваемого свойства, а доступ к объекту осуществляется через args.object.


Доступ к вложенным объектам

Когда валидируемая структура содержит вложенные объекты, args.object всегда ссылается на текущий уровень, на котором выполняется проверка.

class Address {
  street: string;
}

class User {
  address: Address;
}

При валидации поля address.street:

  • args.object будет содержать объект Address;
  • доступ к User напрямую отсутствует на этом уровне.

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


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

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

validate(value: any, args: ValidationArguments) {
  const className = args.targetName;
  return true;
}

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


Ограничения доступа к объекту

Несмотря на наличие args.object, существуют важные ограничения:

  • объект может быть plain object, если не используется class-transformer;
  • типы TypeScript не сохраняются во время выполнения;
  • вложенные структуры могут быть частично преобразованы;
  • при асинхронной валидации состояние объекта не должно изменяться.

Сравнение value и object

Разделение контекста на value и object позволяет отделить локальную и глобальную логику:

  • value — строго текущее поле;
  • object — вся модель данных.

Пример:

validate(value: string, args: ValidationArguments) {
  return value.length > 0 && args.object !== null;
}

Такой подход делает возможной проверку бизнес-правил, зависящих от нескольких полей одновременно.


Использование constraints для передачи параметров

constraints позволяет передавать произвольные параметры в декоратор.

validate(value: number, args: ValidationArguments) {
  const [min, max] = args.constraints;

  return value >= min && value <= max;
}

Пример вызова:

@IsInRange(10, 100)
price: number;

Таким образом, валидатор получает как сам объект, так и конфигурацию поведения.


Взаимодействие с class-transformer

При использовании class-transformer объект в args.object может быть уже преобразован в экземпляр класса:

import { plainToInstance } from "class-transformer";

const user = plainToInstance(User, plainObject);

В этом случае:

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

Типизация ValidationArguments в TypeScript

Для повышения точности работы с объектом часто применяется явное приведение типов:

validate(value: any, args: ValidationArguments) {
  const obj = args.object as User;

  return obj.email !== value;
}

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


Практическое применение доступа к объекту

Наиболее распространённые сценарии:

  • проверка зависимых полей (password / confirmPassword);
  • условная валидация (если isActive === true, поле обязательно);
  • сравнение дат внутри объекта;
  • бизнес-правила, зависящие от состояния всей сущности;
  • кросс-полевые ограничения (min/max, диапазоны, уникальные комбинации).

Пример условной логики на основе объекта

validate(value: string, args: ValidationArguments) {
  const obj = args.object as any;

  if (obj.isRequired) {
    return value !== undefined && value !== null && value !== "";
  }

  return true;
}

Здесь логика валидатора полностью зависит от состояния объекта, а не только от отдельного поля.


Особенности жизненного цикла объекта валидации

Во время выполнения:

  1. создаётся экземпляр объекта;
  2. выполняется трансформация (если подключена);
  3. запускается рекурсивная валидация;
  4. каждый валидатор получает актуальный args.object.

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


Роль ValidationArguments в архитектуре class-validator

ValidationArguments выступает центральным контейнером контекста выполнения, обеспечивая:

  • доступ к текущему значению;
  • доступ к полной модели;
  • передачу параметров декоратора;
  • идентификацию класса и свойства.

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