Динамическая валидация

Принцип динамической валидации

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

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

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

Группы валидации как основной механизм динамики

Одним из ключевых инструментов являются 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

Декоратор @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.


Фабрики 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;
}

Такой подход применяется при сложных сценариях:

  • разные клиенты API
  • мультиарендные системы
  • конфигурационные формы

Динамические кастомные валидаторы

Ключевой механизм расширения — 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 в систему зависимостей.


Частичная валидация через validation options

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

await validate(dto, {
  skipMissingProperties: true,
});

Или:

await validate(dto, {
  whitelist: true,
  forbidNonWhitelisted: true,
});

Эти параметры позволяют изменять строгость проверки без изменения кода DTO.


validateOrReject как механизм строгого режима

При динамических сценариях API часто требуется немедленное прерывание:

import { validateOrReject } from "class-validator";

await validateOrReject(dto, {
  groups: ["create"],
});

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


Наследование DTO и переопределение правил

Динамика также достигается через наследование классов:

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 для условий внутри объекта
  • кастомные валидаторы для бизнес-логики
  • фабрики классов для генерации DTO
  • async-валидаторы для внешних источников
  • options validate() для глобального поведения

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