Что такое DTO-валидация вложенных объектов с class-transformer в NestJS?
Проблема
Когда DTO содержит поле, значение которого само является объектом (или массивом объектов), стандартная связка class-validator + ValidationPipe не проверяет это поле «из коробки». Дело в том, что данные приходят как обычный JSON (plain object), а декораторы class-validator рассчитаны на работу с экземплярами классов. Без дополнительной настройки вложенный объект остаётся обычным Object и его внутренние правила валидации просто игнорируются.
Решение: @Type и @ValidateNested
Чтобы вложенный объект прошёл через ту же цепочку валидации, нужно:
- Вынести вложенную структуру в отдельный класс DTO со своими декораторами
class-validator. - На поле родительского DTO поставить
@ValidateNested()— это скажетclass-validator, что нужно рекурсивно провалидировать значение. - Указать
@Type(() => NestedDto)изclass-transformer— это скажет трансформеру, в экземпляр какого класса превращать plain-объект перед валидацией.
Пример
Без @Type декоратор @ValidateNested не сработает корректно, потому что class-transformer не будет знать, в какой класс превращать вложенные данные (особенно критично для массивов).
Как это работает под капотом
Global ValidationPipe в NestJS обычно настраивается с опцией transform: true. Это заставляет class-transformer вызывать plainToInstance() над входящим телом запроса до валидации. При этом:
- Верхнеуровневый объект превращается в экземпляр DTO благодаря типу параметра контроллера.
- Вложенные поля превращаются в экземпляры своих классов только если явно указан
@Type()— TypeScript стирает типы во время компиляции, и без метаданныхreflect-metadata(либо явного@Type) трансформер не может понять, что за класс использовать для вложенного объекта. - После трансформации
class-validatorобходит граф объекта и благодаря@ValidateNestedспускается на уровень ниже, применяя декораторы вложенного DTO.
Массивы вложенных объектов
Для массивов важно не забыть each: true в @ValidateNested({ each: true }), а также передать в @Type фабрику класса — class-transformer применит её к каждому элементу массива.
Частые связанные настройки
whitelist: trueвValidationPipeудаляет из запроса поля, которых нет в DTO, — это тоже касается вложенных структур, если у вложенного класса тоже есть лишние свойства.forbidNonWhitelisted: trueзаставит вернуть ошибку 400 вместо тихого удаления лишних полей.forbidUnknownValues: trueзащищает от ситуации, когда объект вообще не удалось привести к нужному классу.
Итог
DTO-валидация вложенных объектов — это связка @Type() (class-transformer, отвечает за создание правильного экземпляра класса) и @ValidateNested() (class-validator, отвечает за рекурсивный запуск валидации). Без обоих декораторов вложенные данные либо не валидируются вовсе, либо валидируются некорректно.
Что хочет услышать интервьюер
Понимание, почему вложенные объекты не валидируются автоматически (стирание типов, plain object vs class instance)
Знание связки декораторов @Type() и @ValidateNested()
Знание нюанса с массивами: @ValidateNested({ each: true }) плюс @Type(() => Dto)
Понимание роли ValidationPipe с transform: true и связи с class-transformer
Знание опций whitelist/forbidNonWhitelisted для защиты от лишних полей во вложенных структурах
Пример: Вложенный DTO с @Type и @ValidateNested
import { Type } from 'class-transformer';
import { IsString, IsInt, ValidateNested, IsArray } from 'class-validator';
class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
@IsInt()
zipCode: number;
}
class CreateUserDto {
@IsString()
name: string;
// без @Type() и @ValidateNested() поле address не будет провалидировано
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
// для массива вложенных объектов нужен each: true
@IsArray()
@ValidateNested({ each: true })
@Type(() => AddressDto)
previousAddresses: AddressDto[];
}
Пример: Глобальная настройка ValidationPipe
// main.ts
app.useGlobalPipes(
new ValidationPipe({
transform: true, // включает plainToInstance перед валидацией
whitelist: true, // удаляет свойства, не описанные в DTO
forbidNonWhitelisted: true, // ошибка 400 вместо тихого удаления
}),
);
Типичные ошибки
Ставят только @ValidateNested(), забывая @Type() — вложенный объект остаётся plain object и валидация вложенных полей не срабатывает
Не указывают each: true для массивов вложенных DTO
Забывают, что для работы декораторов нужен reflect-metadata и включённые decorators в tsconfig
Путают class-transformer и class-validator, считая что один декоратор решает обе задачи (трансформацию и валидацию)
Не проверяют глобальную настройку ValidationPipe с transform: true, из-за чего трансформация вообще не выполняется


