昨天我們將 DTO 改為實際存在於執行期的 class,讓 ValidationPipe 成功攔下格式錯誤的請求。今天我們要面對另一個極度困擾開發者的反直覺陷阱。
在設定 new ValidationPipe({ transform: false }) 時,初學 NestJS 的開發者常有一種誤會:「既然關閉了轉換,系統應該就不會執行 class-transformer,而是直接拿前端傳來的原始資料讓 class-validator 進行驗證了吧。」
但這正是產生認知錯亂的開始。
現實卻是驗證明明順利通過了,Controller 拿到的卻依然是完全未經轉換的原始資料——那 ValidationPipe 剛才到底是用什麼資料通過驗證的?
今天,我們就要來拆解這個 NestJS 最易令人誤解的底層設計,釐清 ValidationPipe、class-transformer 與 class-validator 之間的連動機制,以及 transform: false 究竟在幕後做了什麼。
假設我們要建立一篇文章,並希望資料進入 Controller 前完成兩件事:
title)前後的空白去掉。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 資料流中的三個關鍵角色各自負責什麼工作:
class-transformer:負責「實例化與轉換」提供 @Transform() 與 @Type() 等轉換工具。它的主要職責是將傳入的 plain object,轉換為具體的 DTO instance:

class-validator:負責「驗證轉換後的資料」負責處理 @IsString() 與 @IsInt() 等驗證裝飾器。它專注於規則查驗,不會主動對資料進行轉型。例如 @IsInt() 拿到字串 "12" 時只會判定失敗,不會自動將其轉為數字。
ValidationPipe:負責「統籌與調度」作為整個驗證流程的調度中心,當請求進入時,它會:
class-transformer:建立 DTO instance 並執行轉換class-validator:驗證這份「轉換後」的實例當 NestJS 取得 Controller 參數的執行期型別資訊(CreatePostDto metatype)並開始解析請求時,ValidationPipe 在內部的核心處理流程如下:

在中間的「驗證階段」,內部產生的暫時 instance 狀態如下:
CreatePostDto {
title: 'NestJS Pipes', // 已由 @Transform() 處理
viewCount: 12, // 已由 @Type() 轉為數字
visibility: 'draft' // 取得 class 預設值
}
這也就解釋了為什麼字串 "12" 能通過 @IsInt() 的驗證:因為 @IsInt() 檢查的根本不是前端傳來的原始字串,而是 class-transformer 已經幫忙轉換好的數字 12。
關鍵在於最後一步。當驗證順利通過後,transform 設定會決定最後要把哪一份資料交給 Controller:
transform: false 時(預設行為)
驗證成功後,ValidationPipe 會將一開始的「原始 payload」直接傳給 Controller。
因此,Controller 最終拿到的,就只是一個未經轉換的 plain object。
transform: true 時
當設定為 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」。
ValidationPipe 是流程統籌者:它會先透過 class-transformer 建立並轉換暫時的 DTO instance,再交由 class-validator 執行嚴格驗證。transform: false 不會關閉轉換機制:NestJS 依然會在內部建立暫時 DTO 供驗證使用,只是驗證通過後,選擇不將該實例傳給 Controller。12,但 Controller 最後拿到的仍是放行的原始字串 "12"。transform: true:當業務邏輯需要型別轉換結果、類別預設值或類別方法時,必須開啟 transform: true。