在 NestJS 中處理網址查詢參數時,最棘手的問題往往不是驗證被擋下,而是轉型過程發生偏差,導致系統帶著錯誤的值繼續執行後續邏輯。
特別是傳入 ?isPublished=false 這類查詢條件時,後端很常因為誤用了型別轉換機制,默默將其判定為 true,進而拿著錯誤的條件去執行資料庫查詢或業務判斷。
這篇文章我們就來徹底拆解 HTTP 查詢參數的轉型機制,以及如何正確排除這個布林值陷阱。
假設我們要開發一支文章列表 API,讓前端能透過 isPublished 參數篩選發布狀態。
我們先定義 DTO:
import { IsBoolean } from 'class-validator';
export class PostsQueryDto {
@IsBoolean()
isPublished: boolean;
}
Controller 端的宣告與設定如下:
@Get('posts')
getPosts(
@Query(new ValidationPipe({ transform: true }))
query: PostsQueryDto,
) {
return query;
}
這時如果送出:
GET /posts?isPublished=false
我們預期 Controller 最終會拿到轉型後的布林值:
{ "isPublished": false }
但實際上,這個請求會直接被攔下:
{
"message": [
"isPublished must be a boolean value"
],
"error": "Bad Request",
"statusCode": 400
}

這個問題的核心不在 NestJS 本身,也不在 class-validator,而是源於 HTTP 協定本身的特性:
URL query string 裡的值,本質上都是字串。
當你呼叫 GET /posts?isPublished=false 時,NestJS 接收到的 isPublished 原始值其實是 'false'(字串),而不是 false(布林值)。
需要特別注意,@IsBoolean() 是一個型別檢查器,而非轉換器——它只負責驗證輸入是否已經是布林型別,完全不會主動幫忙轉型。當它看到進來的是字串 'false' 時,只會認定型別不符並拋出錯誤。
@Transform() 顯式轉型,再交給驗證器既然查詢參數的原始值一定是字串,解法就非常明確:
先把 'true' / 'false' 字串轉成真正的 boolean,再讓 @IsBoolean() 進行型別驗證。
import { Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';
export class PostsQueryDto {
@Transform(({ value }) => {
if (value === 'true') return true;
if (value === 'false') return false;
return value;
})
@IsBoolean()
isPublished: boolean;
}
這段轉換邏輯的優點在於:
'true' 與 'false' 會被轉為對應的布林值。'1'、'no')會保持原樣傳給 @IsBoolean(),隨後被攔截。Boolean() 或框架底層的轉型。這時,我們再呼叫一次:
GET /posts?isPublished=false
順利拿到預期的布林結果:
{ "isPublished": false }
若有人傳入非法參數:
GET /posts?isPublished=no
API 依然能如期回傳 400 Bad Request 攔下這筆請求。
@Type(() => Boolean) 轉型?許多開發者遇到型別轉換問題時,第一個直覺不是使用 @Transform(),而是使用 @Type():
@Type(() => Boolean)
@IsBoolean()
isPublished: boolean;
看起來很合理,因為這像是在說:「請把它轉成 Boolean。」
但問題就藏在 JavaScript 的 Boolean() 轉型規則裡——任何非空字串轉為 Boolean 時結果全都是 true。
Boolean('true'); // true
Boolean('false'); // true(地雷)
Boolean('0'); // true
Boolean('no'); // true
Boolean(''); // false
@Type(() => Boolean) 底層走的正是這套邏輯,當它拿到 'false' 時,會直接轉成 true!
這時問題就從「驗證失敗」演變成更危險的情況:型別驗證通過了,但值本身已經是錯的。這種靜默失敗(silent failure)比直接回傳 400 更危險,因為 API 表面上成功執行,實則早已拿著錯誤的篩選條件進入資料庫。
有些開發者為了省事,會選擇開啟隱式轉換選項(enableImplicitConversion):
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
})
處理 number 時,這個設定確實很省事,但換成 boolean,這條路同樣行不通。
因為隱式轉換的底層仍然依賴 Boolean(),也就是說,你只是換了一種方式掉進同一個坑。
這裡比較兩種型別在隱式轉換下的行為差異:
| 型別 | 隱式轉換行為 | 風險 |
|---|---|---|
number |
'123' → 123、'abc' → NaN |
相對可控,通常還能再被驗證器擋下 |
boolean |
'true' → true、'false' → true |
高風險,因為語意可能被改錯 |
由此可見,無論是 @Type(() => Boolean) 還是隱式轉換,問題都在於將布林判斷交給了無法識別 'false' 字串語意的原生 Boolean()。
處理 boolean 查詢參數時,最穩妥的做法依然是使用 @Transform() 顯式定義每個字串對應的布林結果。
query string 的原始值永遠是字串:?isPublished=false 進到後端時,'false',不是布林值 false。@IsBoolean() 只驗證,不轉型:它看到字串 'false' 只會報錯,不會幫你轉成布林值。@Type(() => Boolean) 與隱式轉換皆不可靠:兩者底層均依賴 JavaScript 原生 Boolean(),導致非空字串 'false' 被誤判為 true。@Transform() 明確處理:透過顯式比對字串,才能在確保「正確轉型」的同時維持「驗證防線」。