0Pricing
NestJS Enterprise Backend APIs · 강의

중첩 객체 및 배열 DTO 검증

@ValidateNested, @Type 및 화이트리스트 제거를 사용해 중첩 객체와 컬렉션을 검증합니다

중첩 객체 및 배열 DTO 검증은(는) CoddyKit의 무료 NestJS Enterprise Backend APIs 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 NestJS Enterprise Backend APIs 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Nested Validation Needs Help

NestJS pairs class-validator with class-transformer through the global ValidationPipe. For flat DTOs this works out of the box: decorators on each property run automatically.

But the moment a DTO contains another object or an array of objects, validation silently stops at the boundary. The validator sees a child property, but it does not know it is a class instance whose own decorators should run.

  • A nested address object is treated as an opaque value.
  • An items array is checked for being an array, but its elements are never validated.

This lesson shows how @ValidateNested, @Type, and whitelist stripping close those gaps.

The Problem in Code

Consider an order payload with a nested address. Adding @ValidateNested alone is not enough — the validator still needs the child to be an actual AddressDto instance, not a plain object.

Without @Type, class-transformer leaves address as a plain object, so the @IsString() on city never runs. Invalid data passes through unnoticed.

import { IsString, ValidateNested } from 'class-validator';

// AddressDto's own rules will NOT run yet
class AddressDto {
  @IsString()
  city: string;
}

export class CreateOrderDto {
  @ValidateNested() // declares intent, but lacks a target type
  address: AddressDto;
}

@Type Makes the Child a Real Instance

The missing piece is @Type(() => AddressDto) from class-transformer. It tells the transformer how to construct the nested value, turning the raw JSON object into a true AddressDto instance.

Only then do the child's decorators (@IsString, @IsNotEmpty, etc.) actually execute. The rule of thumb:

  • @ValidateNested() → "recurse into this property".
  • @Type(() => Child) → "build it as this class first".

You almost always need both together.

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

class AddressDto {
  @IsString()
  city: string;

  @IsPostalCode('US')
  zip: string;
}

export class CreateOrderDto {
  @ValidateNested()
  @Type(() => AddressDto)
  address: AddressDto;
}

Enabling transform in the Pipe

For @Type to take effect, the ValidationPipe must run class-transformer. Enable it globally with transform: true.

This also converts primitives: a route param string '42' becomes a number when the DTO/param type says so. Without transform, nested classes stay plain and your @Type hints are ignored.

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true, // run class-transformer (required for @Type)
      whitelist: true,
      forbidNonWhitelisted: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

Validating Arrays of DTOs

Arrays of objects need the same pair, with one addition: each: true. This makes @ValidateNested({ each: true }) apply the nested validation to every element of the array.

@Type(() => ItemDto) maps each raw element into an ItemDto instance. Add @IsArray() and optionally @ArrayMinSize(1) to guard the collection shape itself.

import { IsArray, ArrayMinSize, IsString, IsInt, Min, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

class OrderItemDto {
  @IsString()
  sku: string;

  @IsInt()
  @Min(1)
  quantity: number;
}

export class CreateOrderDto {
  @IsArray()
  @ArrayMinSize(1)
  @ValidateNested({ each: true }) // validate every element
  @Type(() => OrderItemDto)
  items: OrderItemDto[];
}

Deeply Nested Structures

The pattern composes to any depth. An order has items, and each item has its own nested discount. Each level that crosses into another class repeats the @ValidateNested + @Type pair.

Validation recurses top-down: the order validates its items array, each item validates its discount object, and every leaf decorator runs. There is no special "deep" flag — you just apply the same two decorators at each boundary.

import { ValidateNested, IsString, IsNumber, Max, Min } from 'class-validator';
import { Type } from 'class-transformer';

class DiscountDto {
  @IsNumber()
  @Min(0)
  @Max(1)
  rate: number;
}

class OrderItemDto {
  @IsString()
  sku: string;

  @ValidateNested()
  @Type(() => DiscountDto)
  discount: DiscountDto;
}

export class CreateOrderDto {
  @ValidateNested({ each: true })
  @Type(() => OrderItemDto)
  items: OrderItemDto[];
}

whitelist Strips Unknown Properties

whitelist: true removes any property in the incoming payload that has no validation decorator on the DTO. This is a core security defense: clients cannot smuggle extra fields like isAdmin or role into your entities.

Crucially, whitelisting works recursively on validated nested objects. If a nested AddressDto only declares city and zip, an injected country field on the nested object is stripped too — but only because @ValidateNested + @Type made the validator descend into it.

forbidNonWhitelisted: Reject vs Strip

There are two postures for unknown fields:

  • whitelist: true — silently strips unknown properties and continues.
  • whitelist + forbidNonWhitelisted: true — rejects the whole request with 400, listing the offending property.

For public APIs, forbidding is stricter and surfaces client mistakes early. Note that forbidNonWhitelisted only has an effect when whitelist is also enabled.

// Payload: { "city": "Austin", "zip": "73301", "hack": "x" }

// whitelist: true (only)
//   -> { city: 'Austin', zip: '73301' }  // 'hack' stripped

// whitelist: true + forbidNonWhitelisted: true
//   -> 400 Bad Request
//   -> message: ["property hack should not exist"]

A Pure class-validator Example You Can Run

Outside NestJS, the same engine works directly. This standalone snippet builds a nested instance with plainToInstance and validates it with validateSync — exactly what the ValidationPipe does internally.

Run it to see how @ValidateNested + @Type surface a nested error.

import 'reflect-metadata';
import { IsString, IsInt, Min, ValidateNested, validateSync } from 'class-validator';
import { plainToInstance, Type } from 'class-transformer';

class ItemDto {
  @IsString() sku!: string;
  @IsInt() @Min(1) quantity!: number;
}

class OrderDto {
  @ValidateNested({ each: true })
  @Type(() => ItemDto)
  items!: ItemDto[];
}

const payload = { items: [{ sku: 'A1', quantity: 0 }] };
const dto = plainToInstance(OrderDto, payload);
const errors = validateSync(dto);

console.log(JSON.stringify(errors[0].children[0].children, null, 2));

Optional and Nullable Nested Objects

A nested object that may be absent should be marked @IsOptional(). When present, it is still validated; when missing, it is skipped. Combine this with @ValidateNested and @Type as usual.

For arrays, @IsOptional() lets the whole array be omitted, while @ArrayMinSize still enforces a minimum once the array is provided. Keep each: true so present elements remain validated.

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

class BillingDto {
  @IsString()
  taxId: string;
}

export class UpdateAccountDto {
  @IsOptional()
  @ValidateNested()
  @Type(() => BillingDto)
  billing?: BillingDto;
}

Common Pitfalls

Watch for these failures, which all let bad data through silently:

  • Forgetting @Type — nested object stays plain; child decorators never run.
  • Forgetting transform: true — the pipe never invokes class-transformer, so @Type is ignored.
  • Missing each: true on arrays — only the first/whole array is checked, not each element.
  • No reflect-metadata import — decorators emit no metadata; validation no-ops.
  • Disabling whitelist — unknown fields flow straight into your service layer.

Quick Check

You have an array of nested DTOs to validate. Which combination is required?

Recap

You now know how to validate nested and array DTOs in NestJS:

  • @ValidateNested() tells the validator to recurse into a child object; add { each: true } for arrays.
  • @Type(() => Child) from class-transformer turns raw JSON into real class instances so child decorators run.
  • transform: true on the ValidationPipe is mandatory for @Type to take effect.
  • whitelist: true strips undeclared properties recursively; forbidNonWhitelisted: true rejects them with a 400 instead.
  • Always import reflect-metadata and repeat the @ValidateNested + @Type pair at every class boundary, however deep.

자주 묻는 질문

“중첩 객체 및 배열 DTO 검증” 강의는 무료인가요?

네 — “중첩 객체 및 배열 DTO 검증” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 NestJS Enterprise Backend APIs 강의 전체를 잠금 해제할 수 있습니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

“중첩 객체 및 배열 DTO 검증”에서 뭘 배우나요?

@ValidateNested, @Type 및 화이트리스트 제거를 사용해 중첩 객체와 컬렉션을 검증합니다 브라우저에서 직접 실행하는 실습 코드로 NestJS Enterprise Backend APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

NestJS Enterprise Backend APIs을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 NestJS Enterprise Backend APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“중첩 객체 및 배열 DTO 검증” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 NestJS Enterprise Backend APIs 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 NestJS Enterprise Backend APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 중첩 객체 및 배열 DTO 검증
  2. 사용자 지정 검증기와 비동기 제약 조건
  3. ClassSerializerInterceptor를 활용한 응답 구성
  4. 조건부 검증과 동적 그룹
← NestJS Enterprise Backend APIs(으)로 돌아가기