Валидация в class-validator изначально строится вокруг
декларативных декораторов, однако в реальных приложениях часто требуется
изменять правила проверки в зависимости от контекста выполнения: роли
пользователя, типа запроса, режима работы API или состояния объекта.
Динамическая валидация в этом контексте означает возможность изменять набор правил валидации без изменения структуры класса, управляя поведением через:
Одним из ключевых инструментов являются validation groups. Они позволяют включать или исключать правила в зависимости от сценария.
import { IsString, IsOptional } from "class-validator";
export class UserDto {
@IsString({ groups: ["create"] })
email: string;
@IsString({ groups: ["create", "update"] })
password: string;
@IsOptional({ groups: ["update"] })
avatar?: string;
}
При вызове валидации можно явно указать группы:
import { validate } from "class-validator";
await validate(userDto, { groups: ["create"] });
await validate(userDto, { groups: ["update"] });
Группы позволяют строить разные режимы проверки одного и того же класса без дублирования DTO.
Декоратор @ValidateIf предоставляет условную активацию
валидаторов на уровне поля.
import { ValidateIf, IsString } from "class-validator";
export class PaymentDto {
@ValidateIf(o => o.method === "card")
@IsString()
cardNumber: string;
}
Здесь правило становится активным только при выполнении условия. Это базовый механизм реактивной схемы валидации, зависящей от состояния объекта.
Функции в ValidateIf, а также в кастомных валидаторах
получают доступ к объекту целиком.
@ValidateIf((obj) => obj.role === "admin")
@IsString()
adminCode: string;
Это позволяет строить зависимости между полями без дополнительной логики вне DTO.
Иногда требуется создавать разные наборы правил программно. В таких случаях используется генерация классов:
import { IsString } from "class-validator";
function createUserDto(isStrict: boolean) {
class DynamicUserDto {
@IsString()
name: string;
password?: string;
}
if (isStrict) {
Reflect.decorate(
[IsString()],
DynamicUserDto.prototype,
"password"
);
}
return DynamicUserDto;
}
Такой подход применяется при сложных сценариях:
Ключевой механизм расширения — registerDecorator. Он
позволяет передавать параметры и создавать адаптивные правила.
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from "class-validator";
function IsGreaterThan(property: string, options?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
name: "isGreaterThan",
target: object.constructor,
propertyName,
constraints: [property],
options,
validator: {
validate(value: any, args: ValidationArguments) {
const relatedValue = (args.object as any)[property];
return typeof value === "number" && value > relatedValue;
},
},
});
};
}
Использование становится контекстно-зависимым:
export class RangeDto {
min: number;
@IsGreaterThan("min")
max: number;
}
Validator может выполнять асинхронные операции: запросы к базе данных, API или кешу.
import { ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
@ValidatorConstraint({ async: true })
export class IsEmailUnique implements ValidatorConstraintInterface {
async validate(email: string) {
const user = await database.findUserByEmail(email);
return !user;
}
}
Регистрация:
import { Validate } from "class-validator";
export class RegisterDto {
@Validate(IsEmailUnique)
email: string;
}
Асинхронность делает возможным динамическое принятие решений на основе внешнего состояния системы.
Часто требуется валидировать одно поле только при наличии другого:
import { ValidateIf, IsNumber } from "class-validator";
export class OrderDto {
couponCode?: string;
@ValidateIf(o => !!o.couponCode)
@IsNumber()
discount: number;
}
Такая логика формирует связные правила между полями объекта, превращая DTO в систему зависимостей.
Динамика также достигается через выборочное выполнение проверок:
await validate(dto, {
skipMissingProperties: true,
});
Или:
await validate(dto, {
whitelist: true,
forbidNonWhitelisted: true,
});
Эти параметры позволяют изменять строгость проверки без изменения кода DTO.
При динамических сценариях API часто требуется немедленное прерывание:
import { validateOrReject } from "class-validator";
await validateOrReject(dto, {
groups: ["create"],
});
Такой подход используется в сервисах, где ошибка валидации должна блокировать дальнейшее выполнение логики.
Динамика также достигается через наследование классов:
export class BaseUserDto {
name: string;
}
export class AdminUserDto extends BaseUserDto {
@IsString()
adminKey: string;
}
Дополнительные правила можно подключать на уровне расширения классов, формируя разные профили данных.
В сложных системах правила валидации могут зависеть от конфигурации:
const config = {
passwordMinLength: 10,
};
Использование в кастомном валидаторе:
validate(value: string) {
return value.length >= config.passwordMinLength;
}
Таким образом, правила валидации становятся изменяемыми без изменения DTO.
На практике динамическая валидация строится не на одном инструменте, а на их комбинации:
groups для сценариев (create/update/admin)ValidateIf для условий внутри объектаvalidate() для глобального поведенияТакой подход позволяет формировать систему проверки данных, которая адаптируется к контексту выполнения и состоянию приложения без дублирования моделей данных.