Валидация данных в сложных прикладных системах редко бывает статичной. Один и тот же объект может проходить разные сценарии проверки в зависимости от этапа жизненного цикла: создание, частичное обновление, админская модификация, импорт данных, публичный 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'],
});
Правило будет активировано, так как присутствует хотя бы одна совпадающая группа.
Контекст особенно важен при вложенных объектах.
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: true позволяет игнорировать групповой
контекст и выполнять правило всегда.
@IsNotEmpty({ always: true })
id: string;
Это используется для критически важных ограничений, которые не должны зависеть от сценария выполнения.
Обе функции поддерживают передачу опций, включающих группы.
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 не является группой, он часто
используется вместе с контекстом для динамического управления
поведением.
create: обязательная полнота данныхupdate: частичная валидацияpatch: минимальные проверкиuseradminsystemКаждый сценарий активирует собственный набор правил без изменения структуры DTO.
Контекст может комбинироваться для создания более точных сценариев.
await validate(dto, {
groups: ['update', 'admin'],
context: {
strictMode: true,
},
});
Такое сочетание позволяет одновременно:
Группы работают только на уровне декларативных валидаторов. Они не влияют на:
Также важно учитывать, что отсутствие группы означает глобальную активацию, что может привести к неожиданным проверкам при расширении модели.
В доменных системах DTO часто становятся универсальными контрактами для нескольких слоёв приложения. Контекст позволяет избежать дублирования классов:
Особенно это критично при микросервисной архитектуре, где один и тот же объект может поступать из разных источников с разными требованиями к строгости проверки.
Если правило входит в несколько групп, оно активируется при совпадении хотя бы одной группы. Конфликты решаются не приоритетом, а объединением условий.
@IsString({ groups: ['a', 'b'] })
field: string;
Запуск с groups: ['b', 'c'] активирует правило.
Если валидация запускается без указания групп, применяются только:
always: trueЭто поведение часто используется в базовых сценариях проверки входных данных, где детализация контекста не требуется.