Что такое DTO-валидация вложенных объектов с class-transformer в NestJS?

MiddleNestJS · Backend·Обновлено 6 сентября 2026
Коротко
Это проверка полей, которые сами являются объектами или массивами объектов внутри DTO: чтобы class-validator корректно обошёл вложенные структуры, их нужно явно превратить в экземпляры класса через class-transformer (@Type) и пометить декоратором @ValidateNested.

Проблема

Когда DTO содержит поле, значение которого само является объектом (или массивом объектов), стандартная связка class-validator + ValidationPipe не проверяет это поле «из коробки». Дело в том, что данные приходят как обычный JSON (plain object), а декораторы class-validator рассчитаны на работу с экземплярами классов. Без дополнительной настройки вложенный объект остаётся обычным Object и его внутренние правила валидации просто игнорируются.

Решение: @Type и @ValidateNested

Чтобы вложенный объект прошёл через ту же цепочку валидации, нужно:

  1. Вынести вложенную структуру в отдельный класс DTO со своими декораторами class-validator.
  2. На поле родительского DTO поставить @ValidateNested() — это скажет class-validator, что нужно рекурсивно провалидировать значение.
  3. Указать @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, из-за чего трансформация вообще не выполняется

Лучшие курсы по теме

изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Node.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее