Валидация данных в объектах часто выходит за рамки простых
независимых проверок отдельных свойств. Реальные структуры данных
нередко требуют логики, при которой валидность одного поля зависит от
значений других. Подобные сценарии называются условной валидацией и
являются одной из ключевых возможностей
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;
}
Такая модель формирует декларативное описание бизнес-правил непосредственно в структуре данных, без необходимости выносить их в отдельные сервисы.