Валидация с контекстом

Валидация данных в сложных прикладных системах редко бывает статичной. Один и тот же объект может проходить разные сценарии проверки в зависимости от этапа жизненного цикла: создание, частичное обновление, админская модификация, импорт данных, публичный API или внутренние сервисные операции. Для управления такими сценариями используется контекст валидации, который в библиотеке class-validator реализуется прежде всего через группы (validation groups), а также дополнительные механизмы конфигурации выполнения правил.

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


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

Принцип групп

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

import { IsEmail, IsString, Length } from 'class-validator';

export class UserDto {
  @IsString()
  @Length(2, 20, { groups: ['create', 'update'] })
  username: string;

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

В данном случае:

  • username проверяется при создании и обновлении
  • email проверяется только при создании

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


Активация групп при выполнении валидации

Группы начинают работать только при явном указании контекста выполнения.

import { validate } from 'class-validator';

await validate(userDto, {
  groups: ['create'],
});

или:

await validate(userDto, {
  groups: ['update'],
});

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


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

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

export class ProductDto {
  @IsString()
  title: string; // всегда валидируется

  @IsString({ groups: ['admin'] })
  internalCode: string; // только для admin
}

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


Множественные группы и комбинирование контекстов

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

@Length(5, 30, { groups: ['create', 'admin', 'import'] })
description: string;

При передаче нескольких групп валидации поведение становится объединяющим:

await validate(dto, {
  groups: ['import', 'admin'],
});

Правило будет активировано, так как присутствует хотя бы одна совпадающая группа.


Использование групп в сложных DTO-структурах

Контекст особенно важен при вложенных объектах.

import { ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

class Address {
  @IsString({ groups: ['create'] })
  city: string;
}

class User {
  @ValidateNested({ groups: ['create'] })
  @Type(() => Address)
  address: Address;
}

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


always и игнорирование групп

Параметр always: true позволяет игнорировать групповой контекст и выполнять правило всегда.

@IsNotEmpty({ always: true })
id: string;

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


Контекст через validateOrReject и validate

Обе функции поддерживают передачу опций, включающих группы.

import { validateOrReject } from 'class-validator';

await validateOrReject(dto, {
  groups: ['update'],
});

Разница заключается в обработке результата:

  • validate возвращает массив ошибок
  • validateOrReject выбрасывает исключение при наличии ошибок

Контекст при этом не меняет модель ошибок, но влияет на их формирование.


Дополнительный пользовательский контекст

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

await validate(dto, {
  context: {
    role: 'admin',
    source: 'internal-api',
  },
});

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


Доступ к контексту внутри кастомных валидаторов

При создании пользовательских валидаторов контекст доступен через ValidationArguments.

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
} from 'class-validator';

@ValidatorConstraint({ name: 'customRule', async: false })
export class CustomRule implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const context = args.object as any;
    return context.role === 'admin' ? true : value !== 'forbidden';
  }

  defaultMessage(args: ValidationArguments) {
    return 'Значение запрещено в текущем контексте';
  }
}

В реальных проектах чаще используют передачу через context в опциях, а не через сам объект, но механизм позволяет учитывать окружение выполнения.


Условная валидация на основе контекста

Сочетание групп и пользовательского контекста позволяет строить условные правила.

@ValidateIf((obj, value) => obj.mode === 'strict')
@IsString()
name: string;

Хотя ValidateIf не является группой, он часто используется вместе с контекстом для динамического управления поведением.


Сценарии применения контекста

Разделение CRUD-операций

  • create: обязательная полнота данных
  • update: частичная валидация
  • patch: минимальные проверки

Разграничение ролей

  • user
  • admin
  • system

Разные источники данных

  • API
  • импорт CSV
  • внутренние сервисы

Каждый сценарий активирует собственный набор правил без изменения структуры DTO.


Вложенные сценарии и композиция контекста

Контекст может комбинироваться для создания более точных сценариев.

await validate(dto, {
  groups: ['update', 'admin'],
  context: {
    strictMode: true,
  },
});

Такое сочетание позволяет одновременно:

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

Ограничения и особенности поведения групп

Группы работают только на уровне декларативных валидаторов. Они не влияют на:

  • преобразование типов
  • сериализацию объектов
  • бизнес-логику вне валидаторов

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


Контекст в сложных доменных моделях

В доменных системах DTO часто становятся универсальными контрактами для нескольких слоёв приложения. Контекст позволяет избежать дублирования классов:

  • единая модель запроса
  • разные правила валидации
  • минимизация расхождений между API и внутренними сервисами

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


Приоритеты и конфликт групп

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

@IsString({ groups: ['a', 'b'] })
field: string;

Запуск с groups: ['b', 'c'] активирует правило.


Поведение при отсутствии контекста

Если валидация запускается без указания групп, применяются только:

  • глобальные валидаторы
  • правила с always: true

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