iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Modern Web

《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記系列 第 14 篇

Day 14|形同虛設的內層防線:為什麼巢狀 DTO 驗證不生效?

  • 分享至 

  • xImage
  •  

接續前兩天對 DTO 類別實例(Day 12)與 ValidationPipe 運作流程(Day 13)的拆解,今天我們進入 API 請求解析中極為常見、也極易出現防禦缺口的資料結構——巢狀 DTO。

當 API 需要接收包含子物件或陣列的資料時,許多開發者會在外層欄位掛上 @ValidateNested(),以為這樣就能自動觸發內層 DTO 的驗證規則。然而現實情況卻是:在 NestJS ValidationPipe 的預設驗證設定下,即使內層資料格式完全不合規,系統卻依然回傳 201 Created 順利通關。

這種「看似設了防線,內部驗證卻靜默失效」的地雷極容易被忽略。今天我們就來拆解為什麼單靠 @ValidateNested() 還不夠,以及如何正確啟用巢狀 DTO 的驗證機制。

問題怎麼發生?

假設我們今天要做一個建立文章的 API,這篇文章除了基本的標題與內容外,還會夾帶一個巢狀的 postMeta 物件來存放 SEO 資訊。

我們先為內層的 SEO 資料定義了 PostMetaDto:

import { IsNotEmpty, IsString, MaxLength } from 'class-validator';

export class PostMetaDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(60)
  seoTitle: string;

  @IsString()
  @MaxLength(160)
  seoDescription: string;
}

接著,將這個內層結構嵌入外層的 CreatePostDto,並掛上 @ValidateNested() 告知 NestJS 進行向下驗證:

import { IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { PostMetaDto } from './post-meta.dto';

export class CreatePostDto {
  @IsString()
  @IsNotEmpty()
  title: string;

  @IsString()
  @IsNotEmpty()
  content: string;

  // 明確告訴守衛,這個屬性裡有其他結構要驗證
  @ValidateNested()
  postMeta: PostMetaDto;
}

在控制器中定義路由:

@Controller('posts')
export class PostsController {
  @Post()
  createPost(@Body() body: CreatePostDto) {
    return body;
  }
}

最後,我們在 main.ts 掛載全域 ValidationPipe,並開啟了 transform: true:

app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
  }),
);

內外層都設妥了驗證規則,這防線看起來很完美,對吧?

現在,我們故意送一筆格式錯誤的請求資料到這支 API:

{
  "title": "My first post",
  "content": "Hello NestJS",
  "postMeta": {
    "seoTitle": "",
    "seoDescription": 12345
  }
}

在這個請求裡,seoTitle 與 seoDescription 都是不該被通過的值。你可能正期待送出請求時拋出 400 Bad Request 。然而,API 卻回傳了 201 Created,這包充滿錯誤的資料,就這樣毫無阻擋地進到了控制器。

也就是說,我們掛在外層的 @ValidateNested() 沒有啟動任何實質的檢查,那些寫在內層的 @IsString() 等裝飾器,全部變成了無效的擺飾。

根因:子包裹未經實例化,分揀員找不到驗證標籤

還記得我們在 Day 12 提到的機場行李託運比喻嗎?

後端的分揀員(ValidationPipe)必須依賴行李上的實體標籤(class 內部的 metadata)才能核對驗證規則。當一個 HTTP 請求進入系統時,請求資料的本質只是普通 JSON,轉換成 JavaScript 後也僅是 plain object。

我們可以延續行李託運的邏輯,來看看這次的巢狀結構發生了什麼事:

  • 外層 DTO(託運的大行李):ValidationPipe 透過 Controller 參數型別(metatype)得知預期的 class,因此能順利將頂層物件轉換為 CreatePostDto 的實例,並核對 title 與 content 的規則。
  • 內層物件(行李內的子箱子):外層欄位掛上的 @ValidateNested(),就像是一張提醒貼紙,對分揀員喊著:「請記得打開 postMeta 這個子箱子檢查裡面的東西!」

然而,當分揀員打開行李拿出 postMeta 時,卻發現這只是一個沒有任何標籤的普通物件(plain object)。

https://ithelp.ithome.com.tw/upload/images/20260928/20184306B0eMQZwT29.png

原因在於:在資料解析階段,我們沒有指定內層物件的轉型規則。class-transformer 預設不會自動將內層普通物件轉換為 PostMetaDto 的類別實例(instance)。

既然 postMeta 只是普通的 JavaScript 物件而非 PostMetaDto 實例,class-validator 就無法從其 metadata 儲存區查出對應的規則。分揀員查不到規則,最後只能將其當作一般物件直接放行,導致內層的驗證機制完全靜默失效。

排雷指南:用 @Type 實例化內層物件

既然根因在於內層資料只是 plain object,解法就是告知 class-transformer 在解析時,確實將其轉化為 PostMetaDto 的類別實例。

負責這項轉型工作的,就是 class-transformer 提供的 @Type() 裝飾器。我們只需在 CreatePostDto 中補上設定:

import { Type } from 'class-transformer'; // 引入 Type
import { IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { PostMetaDto } from './post-meta.dto';

export class CreatePostDto {
  /* ...省略其他屬性... */

  @ValidateNested()
  @Type(() => PostMetaDto) // 指定內層應轉換的類別實例
  postMeta: PostMetaDto;
}

@Type(() => PostMetaDto) 的作用非常明確,它告訴轉換機制:「當解析到 postMeta 這個欄位時,請將傳入的 plain object 實例化為 PostMetaDto 的類別實例。」

當 PostMetaDto 的實例被建立後,掛載於其上的驗證規則才會正式生效。這時 ValidationPipe 進行查驗,就能精準攔截格式錯誤的資料:

{
  "message": [
    "postMeta.seoTitle should not be empty",
    "postMeta.seoDescription must be shorter than or equal to 160 characters",
    "postMeta.seoDescription must be a string"
  ],
  "error": "Bad Request",
  "statusCode": 400
}

總結

  1. 缺少 @Type() 會導致巢狀驗證靜默失效:內層資料若未轉換成 DTO 實例,而仍是 plain object,class-validator 就無法讀取該 DTO 裝飾器所登記的驗證規則,系統會直接跳過內層驗證,不會拋出任何錯誤。
  2. @ValidateNested() 不負責轉型:它僅通知驗證器「向下深入檢查」,必須搭配 @Type() 才能確保內層物件被正確實例化。
  3. @ValidateNested() 與 @Type() 必須搭配使用:前者負責告知驗證器向下檢查,後者負責將內層資料實例化為指定的 DTO 類別。無論處理巢狀物件或物件陣列,兩者都是驗證 HTTP 請求資料時的標準組合。

參考資料


上一篇
Day 13|消失的 Class 實例:transform: false 到底保留了什麼,又丟掉了什麼?
下一篇
Day 15|空值的考驗:明明加了 @IsNotEmpty(),為什麼 null 還是能過?拆解 @IsOptional() 條件驗證的機制
系列文
《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言