Группы валидации

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

Группы задаются строковыми идентификаторами и связываются с конкретными валидаторами через параметр groups, а затем активируются при выполнении функции validate или validateOrReject.


Базовый принцип работы групп

Каждый декоратор в Class-validator может принимать объект конфигурации, содержащий поле groups:

import { IsString, IsNotEmpty, IsEmail } from "class-validator";

export class UserDto {
  @IsString({ groups: ["create", "update"] })
  @IsNotEmpty({ groups: ["create"] })
  name: string;

  @IsEmail({}, { groups: ["create"] })
  email: string;
}

В данном примере:

  • name обязателен только при создании (create)
  • при обновлении (update) допускается отсутствие значения
  • email проверяется только в сценарии создания

Такой подход позволяет одной моделью описывать разные контексты валидации.


Активация групп при валидации

Чтобы группы начали работать, их необходимо передать в параметры функции валидации:

import { validate } from "class-validator";

const dto = new UserDto();
dto.name = "";

validate(dto, { groups: ["create"] }).then(errors => {
  console.log(errors);
});

Если группы не переданы, применяется поведение по умолчанию — используются только валидаторы без указанного groups.


Поведение валидаторов без групп

Декораторы, не содержащие параметр groups, считаются принадлежащими группе по умолчанию.

import { IsString } from "class-validator";

class ProductDto {
  @IsString()
  title: string;
}

В этом случае:

  • title будет валидироваться всегда, независимо от переданных групп
  • такие валидаторы можно считать «глобальными»

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


Комбинация нескольких групп

Один и тот же валидатор может принадлежать нескольким группам одновременно:

import { IsOptional, IsNumber, Min } from "class-validator";

class OrderDto {
  @IsNumber({}, { groups: ["create", "update"] })
  @Min(1, { groups: ["create"] })
  quantity: number;
}

Логика применения:

  • при create активны оба ограничения
  • при update применяется только проверка типа
  • минимальное значение игнорируется

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


Использование групп для различающихся бизнес-сценариев

Создание сущности

import { IsString, IsEmail, IsNotEmpty } from "class-validator";

class RegisterUserDto {
  @IsString({ groups: ["register"] })
  @IsNotEmpty({ groups: ["register"] })
  username: string;

  @IsEmail({}, { groups: ["register"] })
  email: string;
}
validate(dto, { groups: ["register"] });

Частичное обновление

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

class UpdateUserDto {
  @IsOptional({ groups: ["update"] })
  @IsString({ groups: ["update"] })
  username?: string;
}

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


Группы и IsOptional

Особенность сочетания IsOptional и групп часто вызывает ошибки проектирования. Важно учитывать, что:

  • IsOptional влияет только на наличие значения
  • группы определяют, когда правило вообще активируется

Корректный паттерн:

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

class ProfileDto {
  @IsOptional({ groups: ["update"] })
  @IsString({ groups: ["update"] })
  bio?: string;
}

В этом случае:

  • поле игнорируется при отсутствии значения
  • но только в рамках update

Сложные сценарии с разными наборами правил

Группы позволяют строить многоуровневую валидацию одного и того же поля:

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

class PasswordDto {
  @Length(8, 20, { groups: ["register", "change-password"] })
  password: string;

  @Matches(/[A-Z]/, { groups: ["register"] })
  uppercaseRequirement: string;

  @Matches(/[0-9]/, { groups: ["register", "change-password"] })
  numberRequirement: string;
}

Здесь:

  • регистрация требует максимальной сложности
  • смена пароля использует упрощённый набор правил

Группы и наследование DTO

При наследовании классов группы не объединяются автоматически — они сохраняются на уровне декораторов.

class BaseDto {
  @IsString({ groups: ["base"] })
  id: string;
}

class ExtendedDto extends BaseDto {
  @IsString({ groups: ["extended"] })
  extra: string;
}

При валидации:

  • группа base влияет только на id
  • группа extended влияет только на extra

Комбинирование групп требует явного указания:

validate(dto, { groups: ["base", "extended"] });

Поведение при отсутствии пересечения групп

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

validate(dto, { groups: ["unknown"] });

Результат:

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

Это поведение важно учитывать при динамическом формировании групп.


Практика проектирования групп

На уровне архитектуры часто выделяются стандартные наборы:

  • create — строгая валидация всех обязательных полей
  • update — частичная валидация
  • delete — проверка идентификаторов
  • auth — сценарии авторизации
  • internal — системные операции

Пример унифицированной модели:

class AccountDto {
  @IsString({ groups: ["create", "update", "auth"] })
  login: string;

  @IsString({ groups: ["create"] })
  password: string;

  @IsString({ groups: ["update"] })
  newPassword: string;
}

Ошибки проектирования групп

Типичные проблемы:

  1. Смешивание контекстов в одном поле

    • одно поле используется в разных бизнес-сценариях без разделения логики
  2. Чрезмерное дробление групп

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

    • приводит к неожиданному поведению при вызове validate без параметров
  4. Дублирование правил

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

Динамическое формирование групп

В реальных приложениях группы часто определяются на основе контекста запроса:

function getValidationGroups(action: string): string[] {
  switch (action) {
    case "create":
      return ["create"];
    case "update":
      return ["update"];
    default:
      return [];
  }
}

И затем используются при валидации:

validate(dto, { groups: getValidationGroups(action) });

Взаимодействие групп с кастомными валидаторами

Кастомные валидаторы также поддерживают группы:

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

@ValidatorConstraint({ name: "customRule", async: false })
class CustomRule implements ValidatorConstraintInterface {
  validate(value: any) {
    return value === "allowed";
  }
}

И подключение:

import { Validate } from "class-validator";

class SampleDto {
  @Validate(CustomRule, { groups: ["special"] })
  field: string;
}

Итоговая модель поведения системы групп

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