Создание условий на основе других полей

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

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


Базовый механизм ValidateIf

Наиболее прямой способ задать условие применения валидаторов — декоратор @ValidateIf.

Он позволяет указать функцию, возвращающую логическое значение. Если результат true, последующие валидаторы для поля выполняются. Если false, валидация поля полностью пропускается.

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

class UserDto {
  @ValidateIf(o => o.isActive === true)
  @IsNotEmpty()
  @IsString()
  nickname: string;

  isActive: boolean;
}

В этом примере поле nickname проверяется только при активном пользователе. Если isActive равно false, проверки IsNotEmpty и IsString не применяются.

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


Условия на основе нескольких полей

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

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

  @ValidateIf(o => o.paymentMethod === 'card')
  @IsNotEmpty()
  cvv: string;

  paymentMethod: 'card' | 'cash';
}

Здесь валидация полей cardNumber и cvv активируется только при выборе оплаты картой. При оплате наличными эти поля фактически исключаются из проверки.

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


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

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

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

@ValidatorConstraint({ name: 'matchField', async: false })
class MatchFieldConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const object = args.object as any;
    const [relatedPropertyName] = args.constraints;

    return value === object[relatedPropertyName];
  }

  defaultMessage(args: ValidationArguments) {
    return `Значение не совпадает с ${args.constraints[0]}`;
  }
}

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

function MatchField(property: string) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      constraints: [property],
      validator: MatchFieldConstraint,
    });
  };
}

class RegisterDto {
  password: string;

  @MatchField('password')
  confirmPassword: string;
}

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


Условные проверки с isDefined и isOptional

Часто условная логика комбинируется с поведением “необязательных” полей. В class-validator для этого используются @IsOptional() и проверки на undefined.

import { IsOptional, IsString, MinLength } from 'class-validator';

class ProfileDto {
  @IsOptional()
  @IsString()
  @MinLength(3)
  displayName?: string;
}

@IsOptional() автоматически пропускает все последующие валидаторы, если значение отсутствует (undefined или null в зависимости от конфигурации трансформации).

Однако важно различать:

  • IsOptional — пропуск валидации при отсутствии значения
  • ValidateIf — динамическое условие, зависящее от состояния объекта

Зависимость через вычисляемые условия

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

class SubscriptionDto {
  plan: 'free' | 'pro' | 'enterprise';

  @ValidateIf(o => o.plan !== 'free')
  @IsNotEmpty()
  billingEmail: string;

  @ValidateIf(o => o.plan === 'enterprise')
  @IsNotEmpty()
  accountManagerId: string;
}

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


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

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

import { ValidateNested, ValidateIf } from 'class-validator';
import { Type } from 'class-transformer';

class AddressDto {
  city: string;
  street: string;
}

class OrderDto {
  @ValidateIf(o => o.deliveryType === 'courier')
  @ValidateNested()
  @Type(() => AddressDto)
  address: AddressDto;

  deliveryType: 'pickup' | 'courier';
}

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


Использование groups как альтернатива условиям

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

import { IsString } from 'class-validator';

class UserDto {
  @IsString({ groups: ['create'] })
  username: string;

  @IsString({ groups: ['update'] })
  id: string;
}

В зависимости от переданной группы при вызове validate, активируется соответствующий набор правил. Это позволяет разделять сценарии создания и обновления без изменения структуры классов.


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

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

@ValidateIf(o => o.mode === 'strict')
@CustomValidator()
value: string;

Порядок применения имеет значение: сначала проверяется условие, затем запускается валидатор. Если условие ложно, весь пайплайн для поля пропускается.


Контекст объекта в условных проверках

Функции условий получают доступ к текущему объекту через аргумент o. Это позволяет строить проверки, зависящие от состояния нескольких полей одновременно.

@ValidateIf(o => o.startDate && o.endDate)

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


Особенности выполнения цепочек валидаторов

При использовании ValidateIf важно учитывать, что:

  • условие вычисляется до запуска остальных валидаторов поля
  • при false остальные декораторы игнорируются полностью
  • вложенные объекты не валидируются, если само свойство исключено условием
  • порядок декораторов критичен: ValidateIf должен располагаться выше остальных проверок
@ValidateIf(o => o.enabled)
@IsString()
@IsNotEmpty()
field: string;

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


Детерминированные и недетерминированные условия

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

Корректный подход опирается только на:

  • значения текущего объекта
  • синхронные вычисления
  • неизменяемую бизнес-логику

Это обеспечивает воспроизводимость результата валидации при повторных вызовах.


Сложные зависимости в доменных моделях

В реальных доменных схемах условная валидация часто отражает бизнес-ограничения:

  • обязательность реквизитов при определённом типе клиента
  • наличие дополнительных данных при расширенном тарифе
  • взаимосвязь временных интервалов
  • альтернативные способы заполнения данных
class EventDto {
  type: 'online' | 'offline';

  @ValidateIf(o => o.type === 'offline')
  @IsNotEmpty()
  location: string;

  @ValidateIf(o => o.type === 'online')
  @IsNotEmpty()
  url: string;
}

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