Ленивая валидация

Поведение валидации по умолчанию

Библиотека Class-validator выполняет проверку объектов по модели, которая изначально ориентирована на полноформатную (eager) валидацию: при вызове validate() или validateOrReject() анализируются все декорированные свойства класса, формируется полный список ошибок без прерывания процесса.

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

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

Ленивая валидация в контексте Class-validator — это набор практик и встроенных механизмов, позволяющих сократить количество выполняемых проверок, прекратить валидацию на раннем этапе или избегать ненужных вычислений для части полей.


Прерывание валидации на первой ошибке

Одним из базовых механизмов ленивого поведения является опция stopAtFirstError.

import { validate } from "class-validator";

await validate(user, {
  stopAtFirstError: true,
});

При включении данного режима:

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

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


Условная валидация через validateIf

Механизм validateIf позволяет полностью исключить выполнение валидаторов для свойства, если условие возвращает false.

import { validateIf, IsEmail } from "class-validator";

class User {
  isEmailRequired: boolean;

  @validateIf(o => o.isEmailRequired)
  @IsEmail()
  email: string;
}

Логика работы:

  • функция-условие выполняется первой,
  • при false все последующие декораторы игнорируются,
  • отсутствует создание ошибок и запуск встроенных проверок.

Этот механизм часто используется для:

  • частично заполненных форм,
  • сценариев обновления сущностей (PATCH),
  • динамических DTO, зависящих от контекста.

Группы как механизм ленивого выполнения

Группы (groups) позволяют разделять наборы правил валидации и выполнять только часть из них.

import { IsString, Length } from "class-validator";

class User {
  @IsString({ groups: ["create"] })
  @Length(10, 20, { groups: ["create"] })
  password: string;

  @IsString({ groups: ["update"] })
  @Length(6, 20, { groups: ["update"] })
  nickname: string;
}

Вызов:

validate(user, { groups: ["update"] });

Особенности ленивого поведения через группы:

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

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


Игнорирование отсутствующих полей

Опция skipMissingProperties снижает объём проверок за счёт исключения отсутствующих значений:

validate(user, {
  skipMissingProperties: true,
});

Поведение:

  • свойства с undefined не проходят валидацию,
  • декораторы для них не запускаются,
  • отсутствующие данные считаются допустимыми.

Это особенно эффективно при:

  • частичных обновлениях,
  • ленивой загрузке данных,
  • обработке неполных DTO из внешних источников.

Пользовательские валидаторы с ранним выходом

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

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from "class-validator";

@ValidatorConstraint({ name: "isEven", async: false })
class IsEvenConstraint implements ValidatorConstraintInterface {
  validate(value: number) {
    if (value == null) return true;
    if (value % 2 !== 0) return false;
    return true;
  }

  defaultMessage() {
    return "Значение должно быть чётным";
  }
}

Ленивые паттерны внутри validate:

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

В сложных системах кастомные валидаторы становятся основным инструментом оптимизации.


Отложенная валидация через validateIf и вычисляемые зависимости

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

class Payment {
  method: string;

  @validateIf(o => o.method === "card")
  cardNumber: string;

  @validateIf(o => o.method === "bank")
  iban: string;
}

Такая модель:

  • предотвращает запуск лишних проверок,
  • уменьшает количество ошибок, не относящихся к текущему сценарию,
  • делает структуру DTO контекстно-ориентированной.

Минимизация валидации при трансформации объектов

При использовании связки с class-transformer ленивость может усиливаться за счёт ограничения входных данных до минимально необходимого набора:

import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";

const dto = plainToInstance(User, payload, {
  excludeExtraneousValues: true,
});

await validate(dto);

Эффект:

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

Ленивые сценарии в глубоко вложенных структурах

При работе с вложенными DTO ленивость достигается комбинацией:

  • validateNested,
  • validateIf,
  • групп,
  • остановки на первой ошибке.
class Address {
  @IsString()
  city: string;
}

class User {
  @validateIf(o => o.includeAddress)
  address: Address;
}

Особенности:

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

Комбинирование механизмов ленивой валидации

На практике ленивое поведение формируется не одним инструментом, а их сочетанием:

  • stopAtFirstError — ограничение глубины диагностики,
  • validateIf — исключение веток валидации,
  • groups — сегментация сценариев,
  • skipMissingProperties — игнорирование пустых значений,
  • кастомные валидаторы — контроль локальной логики,
  • трансформация входных данных — сокращение поверхности проверки.

Типовой оптимизированный сценарий:

await validate(user, {
  groups: ["update"],
  skipMissingProperties: true,
  stopAtFirstError: true,
});

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