Декоратор @ValidateIf

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

Основная идея заключается в том, что @ValidateIf не выполняет валидацию сам по себе, а определяет, будут ли применены последующие декораторы к полю.


Принцип работы

@ValidateIf принимает функцию-предикат, которая получает текущий объект и значение поля. Возвращаемое значение определяет, нужно ли применять остальные валидаторы:

  • true — валидация продолжается
  • false — все последующие валидаторы для этого свойства игнорируются

Сигнатура:

ValidateIf(condition: (object: any, value: any) => boolean)

Важно учитывать порядок выполнения: @ValidateIf должен находиться выше других декораторов в цепочке, так как он влияет на их выполнение.


Базовый пример использования

import { ValidateIf, IsNotEmpty, IsEmail } from 'class-validator';

class User {
  @ValidateIf(o => o.isEmailRequired === true)
  @IsNotEmpty()
  @IsEmail()
  email: string;

  isEmailRequired: boolean;
}

В этом примере поле email проверяется только в случае, если isEmailRequired равно true. Если условие ложно, IsNotEmpty и IsEmail не выполняются.


Контекст выполнения условия

Функция внутри @ValidateIf получает два аргумента:

  • object — текущий экземпляр класса
  • value — значение текущего свойства
@ValidateIf((obj, value) => obj.mode === 'strict' && value !== undefined)

Использование value полезно для проверки наличия данных, а object — для логики, зависящей от других полей.


Условная обязательность поля

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

class Payment {
  @ValidateIf(o => o.method === 'card')
  @IsNotEmpty()
  cardNumber: string;

  method: string;
}

Здесь cardNumber становится обязательным только при выборе метода оплаты card.

Если method !== 'card', поле полностью исключается из проверки.


Взаимодействие с другими валидаторами

@ValidateIf влияет только на выполнение следующих декораторов. Сам по себе он не изменяет значение поля и не вмешивается в его преобразование.

Пример цепочки:

@ValidateIf(o => o.enabled)
@IsString()
@Length(5, 20)
value: string;

Если enabled === false, то ни IsString, ни Length не выполняются.


Поведение при отсутствии значения

Если значение поля отсутствует (undefined), поведение зависит от условия:

@ValidateIf((o, v) => v !== undefined)
@IsNotEmpty()
field: string;

В этом случае валидация не будет выполняться для undefined. Однако это не делает поле автоматически валидным — оно просто исключается из проверки.


Использование с вложенными объектами

@ValidateIf может работать и с вложенными структурами, если они корректно типизированы.

class Address {
  @ValidateIf(o => o.required === true)
  @IsNotEmpty()
  street: string;

  required: boolean;
}

class User {
  address: Address;
}

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


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

1. Альтернативные поля

Когда одно из нескольких полей должно быть валидным:

class Search {
  @ValidateIf(o => !o.query)
  @IsNotEmpty()
  id: string;

  @ValidateIf(o => !o.id)
  @IsNotEmpty()
  query: string;
}

Так обеспечивается логика «либо ID, либо строковый запрос».


2. Режимы работы системы

class Config {
  @ValidateIf(o => o.env === 'production')
  @IsNotEmpty()
  apiKey: string;

  env: string;
}

Валидация активируется только в production-среде.


3. Частично заполненные формы

class Profile {
  @ValidateIf(o => o.subscribe === true)
  @IsEmail()
  email: string;

  subscribe: boolean;
}

Поле email становится актуальным только при включённой подписке.


Особенности выполнения

Отложенная проверка

Функция условия выполняется во время валидации, а не при создании объекта. Это означает, что изменение свойств объекта до вызова validate() влияет на результат.


Независимость от других декораторов

@ValidateIf не зависит от типа данных или порядка других валидаторов, но строго влияет на их выполнение.


Поведение при нескольких декораторах

Если на одном поле несколько @ValidateIf, применяется только ближайший к полю в цепочке.

@ValidateIf(o => o.a)
@ValidateIf(o => o.b)
@IsString()
value: string;

Фактически учитывается только первый декоратор, ближайший к свойству.


Логические ошибки при использовании

1. Сложные условия без контроля

@ValidateIf(o => o.a && o.b || o.c && !o.d)

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


2. Зависимость от мутируемых данных

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


3. Использование побочных эффектов

Функция условия должна быть чистой. Любые изменения состояния объекта внутри неё приводят к нестабильной работе.


Совместимость с другими декораторами class-validator

@ValidateIf часто используется совместно с:

  • @IsNotEmpty
  • @IsEmail
  • @Length
  • @IsOptional (в некоторых сценариях альтернативен, но не эквивалентен)

Важно различать:

  • @IsOptional() пропускает null и undefined
  • @ValidateIf() управляет всей цепочкой валидаторов на основе условия

Различие между @ValidateIf и @IsOptional

@IsOptional работает по принципу:

  • если значение отсутствует → не валидировать дальше
  • если значение есть → валидировать

@ValidateIf:

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

Пример различия:

@IsOptional()
@IsEmail()
email: string;
@ValidateIf(o => o.mode === 'email')
@IsEmail()
email: string;

Во втором случае логика не зависит от наличия значения, а от внешнего состояния.


Производственные практики

При проектировании схем с @ValidateIf важно учитывать:

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

Поведение при сериализации и трансформации

@ValidateIf не влияет на class-transformer и не изменяет структуру объекта. Он работает только на этапе валидации, после того как объект уже создан и преобразован.

Это позволяет разделять ответственность:

  • трансформация данных — отдельный слой
  • условная валидация — отдельный слой логики

Расширенные сценарии

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

class Group {
  @ValidateIf(o => o.members?.length > 0)
  @IsNotEmpty({ each: true })
  members: string[];
}

Проверка массива выполняется только если он содержит элементы.


Зависимость от вложенных значений

class Order {
  @ValidateIf(o => o.payment?.status === 'pending')
  @IsNotEmpty()
  transactionId: string;

  payment: {
    status: string;
  };
}

Такой подход позволяет учитывать состояние вложенных объектов.


Поведение при асинхронных условиях

Функция @ValidateIf должна быть синхронной. Асинхронные проверки не поддерживаются напрямую. Любая попытка использовать промисы или async-функции приведёт к некорректной работе валидации.