iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Modern Web

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

Day 13|消失的 Class 實例:transform: false 到底保留了什麼,又丟掉了什麼?

  • 分享至 

  • xImage
  •  

昨天我們將 DTO 改為實際存在於執行期的 class,讓 ValidationPipe 成功攔下格式錯誤的請求。今天我們要面對另一個極度困擾開發者的反直覺陷阱。

在設定 new ValidationPipe({ transform: false }) 時,初學 NestJS 的開發者常有一種誤會:「既然關閉了轉換,系統應該就不會執行 class-transformer,而是直接拿前端傳來的原始資料讓 class-validator 進行驗證了吧。」

但這正是產生認知錯亂的開始。

現實卻是驗證明明順利通過了,Controller 拿到的卻依然是完全未經轉換的原始資料——那 ValidationPipe 剛才到底是用什麼資料通過驗證的?

今天,我們就要來拆解這個 NestJS 最易令人誤解的底層設計,釐清 ValidationPipe、class-transformer 與 class-validator 之間的連動機制,以及 transform: false 究竟在幕後做了什麼。

問題怎麼發生?

假設我們要建立一篇文章,並希望資料進入 Controller 前完成兩件事:

  1. 把標題(title)前後的空白去掉。
  2. 把字串形式的瀏覽次數(viewCount)轉成真正的數字型別。

這時,我們的 DTO 定義如下:

import { Transform, TransformFnParams, Type } from 'class-transformer';
import { IsIn, IsInt, IsString } from 'class-validator';

function trimString(value: unknown): unknown {
  return typeof value === 'string' ? value.trim() : value;
}

export class CreatePostDto {
  @Transform(({ value }: TransformFnParams) => trimString(value))
  @IsString()
  title: string;

  @Type(() => Number)
  @IsInt()
  viewCount: number;

  @IsIn(['draft'])
  visibility = 'draft';

  createSlug(): string {
    return this.title.toLowerCase().replace(/\s+/g, '-');
  }
}

接著,我們在 Controller 明確加上 transform: false 的設定,並印出最後拿到的結果:

@Post('transform-off')
createWithTransformOff(
  @Body(new ValidationPipe({ transform: false })) body: CreatePostDto,
) {
  return {
    isDtoInstance: body instanceof CreatePostDto,
    hasCreateSlug: typeof body.createSlug === 'function',
    receivedBody: body,
  };
}

此時,我們刻意送出一個帶有空白字串與字串數字的請求:

curl -X POST http://localhost:3000/posts/transform-off \
  -H "Content-Type: application/json" \
  -d '{
    "title": "  NestJS Pipes  ",
    "viewCount": "12"
  }'

照理來說,既然 DTO 宣告了 @Transform() 與 @Type(),Controller 應該要拿到去掉空白後的標題與數字型別的 12。然而,API 實際回傳的結果卻是:

{
  "isDtoInstance": false,
  "hasCreateSlug": false,
  "receivedBody": {
    "title": "  NestJS Pipes  ",
    "viewCount": "12"
  }
}

這串回應揭露了幾個現象:

  • title 的空白完全沒去掉。
  • viewCount 依然是字串 "12"。
  • visibility 的預設值消失了。
  • instanceof CreatePostDto 為 false,它只是一個 plain object。
  • 類別方法 createSlug() 根本不存在。

最詭異的是,既然 Controller 拿到的 viewCount 還是字串 "12",那它剛才到底是怎麼通過 @IsInt() 這個驗證的?

幕後分工:轉換、驗證與調度的鐵三角

要解開這個謎團,我們必須先分清楚 NestJS 資料流中的三個關鍵角色各自負責什麼工作:

1. class-transformer:負責「實例化與轉換」

提供 @Transform() 與 @Type() 等轉換工具。它的主要職責是將傳入的 plain object,轉換為具體的 DTO instance:

https://ithelp.ithome.com.tw/upload/images/20260927/20184306l0C0M5YThW.png

2. class-validator:負責「驗證轉換後的資料」

負責處理 @IsString() 與 @IsInt() 等驗證裝飾器。它專注於規則查驗,不會主動對資料進行轉型。例如 @IsInt() 拿到字串 "12" 時只會判定失敗,不會自動將其轉為數字。

3. ValidationPipe:負責「統籌與調度」

作為整個驗證流程的調度中心,當請求進入時,它會:

  1. 呼叫 class-transformer:建立 DTO instance 並執行轉換
  2. 呼叫 class-validator:驗證這份「轉換後」的實例

ValidationPipe 的內部處理流程

當 NestJS 取得 Controller 參數的執行期型別資訊(CreatePostDto metatype)並開始解析請求時,ValidationPipe 在內部的核心處理流程如下:

https://ithelp.ithome.com.tw/upload/images/20260927/20184306A3XS4e4150.png

在中間的「驗證階段」,內部產生的暫時 instance 狀態如下:

CreatePostDto {
  title: 'NestJS Pipes',  // 已由 @Transform() 處理
  viewCount: 12,          // 已由 @Type() 轉為數字
  visibility: 'draft'     // 取得 class 預設值
}

這也就解釋了為什麼字串 "12" 能通過 @IsInt() 的驗證:因為 @IsInt() 檢查的根本不是前端傳來的原始字串,而是 class-transformer 已經幫忙轉換好的數字 12。

關鍵在於最後一步。當驗證順利通過後,transform 設定會決定最後要把哪一份資料交給 Controller:

設定為 transform: false 時(預設行為)

https://ithelp.ithome.com.tw/upload/images/20260927/20184306eUbjlTOjeN.png

驗證成功後,ValidationPipe 會將一開始的「原始 payload」直接傳給 Controller。

因此,Controller 最終拿到的,就只是一個未經轉換的 plain object。

設定為 transform: true 時

https://ithelp.ithome.com.tw/upload/images/20260927/20184306lTgUsFMIqV.png

當設定為 transform: true 時,ValidationPipe 在驗證成功後,會將轉換完成的 DTO instance 直接回傳給 Controller。

發送相同的 payload,Controller 就能取得完整的物件狀態:

{
  "isDtoInstance": true,
  "hasCreateSlug": true,
  "receivedBody": {
    "title": "NestJS Pipes",
    "viewCount": 12,
    "visibility": "draft"
  }
}

兩者的本質差異如下表所示:

行為 / 特性 transform: false transform: true
執行 class-transformer 轉換 是 是
執行 class-validator 驗證 是 是
Controller 收到轉換後的值 否 是
Controller 收到 DTO instance 否 是
包含類別預設值與內部方法 否 是

這帶出了一個核心觀念:

transform: false 並不是「關閉轉換與驗證」,而是「預設不把處理好的 DTO instance 交給 Controller」。

總結

  1. ValidationPipe 是流程統籌者:它會先透過 class-transformer 建立並轉換暫時的 DTO instance,再交由 class-validator 執行嚴格驗證。
  2. transform: false 不會關閉轉換機制:NestJS 依然會在內部建立暫時 DTO 供驗證使用,只是驗證通過後,選擇不將該實例傳給 Controller。
  3. 驗證通過,不代表 Controller 拿到的值相同:驗證器檢查的可能是轉換後的數字 12,但 Controller 最後拿到的仍是放行的原始字串 "12"。
  4. 依賴 DTO instance 行為時,請明確設定 transform: true:當業務邏輯需要型別轉換結果、類別預設值或類別方法時,必須開啟 transform: true。

參考資料


上一篇
Day 12|失靈的守衛:為什麼 DTO 不能用 interface 和 type?
下一篇
Day 14|形同虛設的內層防線:為什麼巢狀 DTO 驗證不生效?
系列文
《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言