JSONPath 過濾器進階:遞迴、切片與多條件篩選
結論:遞迴下降、陣列切片與過濾器組合起來才是 JSONPath 的真正能力,也是誤用最集中的地方。本文用可執行樣例拆解多條件篩選、結果形態與各庫差異。

結論:先用確定的層級定位、再疊加切片與過濾條件,不要一上來就寫 $..;過濾器裡的裸路徑只判存在,判布林真值必須寫 @.stock == true。
單條
$.store.book[0].title誰都會寫;真正把 JSONPath 用出問題的是$..疊加切片和多個條件之後——結果多了、少了,或者順序完全不是你以為的那樣。
一、先把樣例準備好
{
"store": {
"book": [
{ "title": "JSON 權威指南", "price": 8.95, "stock": true, "tags": ["json", "spec"] },
{ "title": "資料交換格式史", "price": 12.99, "stock": false, "tags": ["history"] },
{ "title": "解析器實現原理", "price": 8.99, "stock": true, "tags": ["parser", "json"] },
{ "title": "串流解析實踐", "price": 9.5, "stock": true, "tags": [] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"updatedAt": "2026-10-31"
}
下面所有表示式都以這份文件為輸入。先記住兩個前提:結果是節點集,順序遵循文件順序而不是條件書寫的順序;過濾器裡的 @ 只代表當前候選節點。
二、遞迴下降 $..:方便,但代價最高
.. 表示「從當前位置開始,對任意深度遞迴查找」。RFC 9535 把它定義為 descendant segment,它可以和名字、通配、下標、切片、過濾器組合:
$..price // 所有層級裡名為 price 的成員
$..book[?(@.price < 10)] // 任意層級下的 book,再過濾
$..[?(@.stock)] // 任意層級下 stock 存在的節點
需要理解三件事:
$..book找到的是所有名為book的節點,不管它在第幾層;..的遍歷是深度優先的,輸出順序依賴實現訪問子節點的順序;..無法利用任何索引,複雜度直接是整棵文件的節點數。
因此 $..price 與 $.store.book[*].price 在結果上可能只差 bicycle 的價格,在效能上卻差一個數量級。能確定層級時,永遠優先寫確定的路徑。如果只是想比較兩套路徑語言的能力邊界,可以對照 JSONPath 與 JSON Pointer 的選擇。
三、陣列切片與通配符:組合時的三個陷阱
切片語法是 [start:end:step],含頭不含尾,負數從尾部計數:
| 寫法 | 含義 | 結果(以 4 本書為例) |
|---|---|---|
[0:2] |
前兩本 | 第 1、2 本 |
[1:] |
從第 2 本到末尾 | 第 2、3、4 本 |
[:2] |
前兩本 | 第 1、2 本 |
[-2:] |
最後兩本 | 第 3、4 本 |
[::2] |
每隔一本取一本 | 第 1、3 本 |
[::-1] |
逆序 | 第 4、3、2、1 本 |
三個陷阱:
- 步長為 0 非法。RFC 9535 要求 step 非零,部分實現會靜默返回空集;
- 負步長不是所有庫都支援。Goessner 原版支援,個別精簡實現只接受正步長;
- 切片後再過濾,範圍不會擴大。
[0:2][?(@.stock)]只在切出來的兩本裡篩選,不會回到整個陣列。
通配符 [*] 與切片可以疊加,但 [*] 得到的是「所有子節點」,如果子節點是物件,後面接 .title 就是所有 title;如果子節點本身是陣列,.title 不會自動再展開一層。需要展開就再寫一次 [*],不要指望隱式扁平化。
四、多條件篩選:比較、邏輯與存在性
過濾器 [?(...)] 裡能寫四類東西:比較、邏輯運算、存在性判斷和函式。一個常見的多條件查詢:
$..book[?(@.price < 10 && @.stock)]
它等價於「價格低於 10 並且 stock 為真」。注意幾個語意細節:
@.stock是存在性判斷,不是「值為 true」。欄位存在但值為false時,這個條件仍然成立。想表達布林真值必須寫@.stock == true;&&的優先順序高於||,混用時加括號,不要靠記憶;- 比較兩邊的型別必須一致。數字與字串不會做隱式轉換,
@.price < "10"在嚴格實現裡是 false 或直接報錯; - 缺失欄位參與比較一律為 false。
@.missing < 10不會報錯,只是不匹配任何節點。
這也是「把過濾條件寫成 JavaScript」最容易翻車的地方:JavaScript 裡 if (obj.stock) 判的是真值,JSONPath 裡判的是存在性。關於標準語法與函式的完整說明,可以回看 JSONPath 查詢語言實戰。
五、指令碼表示式:能力越大,越不該用
Goessner 原版和 jsonpath-plus 支援用 [?(@.price < $.max)] 這種跨節點比較,甚至允許任意指令碼。RFC 9535 明確只允許訪問當前節點 @,不允許 $ 出現在過濾器內部。
// 老實現可以,RFC 9535 不允許
$..book[?(@.price < $.expensive)]
// 標準做法:閾值由呼叫方傳入
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })
指令碼表示式帶來三個問題:
- 可移植性歸零。換一個庫就要重寫全部表示式;
- 安全邊界消失。表示式由使用者輸入時,指令碼求值等於把程式碼執行權交出去;
- 無法靜態最佳化。實現只能逐節點解釋執行,慢且難以快取。
如果確實需要跨節點比較,把閾值作為查詢參數、在應用層做二次過濾,通常比依賴擴充語法更穩。
六、結果形態:你以為拿到的是值,其實拿到的是節點
同一個表示式在不同庫裡的返回值至少有三種形態:
["JSON 權威指南", "解析器實現原理", "串流解析實踐"] // 值陣列
[{ "path": "$['store']['book'][0]['title']", "value": "..." }] // 路徑 + 值
[{ "pointer": "/store/book/0/title", "value": "..." }] // JSON Pointer + 值
這直接影響三件事:前端取值、結果去重,以及把查詢結果轉成寫操作。RFC 9535 定義的 nodelist 是「節點引用」,但 JavaScript 實現通常只返回值,因為引用無法跨程序傳遞。
實踐建議:
- 取單值統一走
[0]或value(),不要假設結果一定是純量; - 需要回寫時選返回 JSON Pointer 的實現,或自己把路徑拼出來;
- 結果為空和結果為
[null]是兩回事,判斷時用長度而不是真值。
七、各庫差異:遷移前先跑一致性用例
| 能力 | Goessner 原版 | jsonpath-plus | RFC 9535 嚴格實現 |
|---|---|---|---|
過濾器括號 ?() |
必需 | 相容 | 可省略 |
跨根引用 $.x |
支援 | 支援 | 禁止 |
| 指令碼表示式 | 支援 | 支援 | 禁止 |
| 負下標與負步長 | 支援 | 支援 | 支援 |
| 返回路徑 | 可配置 | 可配置 | 規範未規定 |
函式 length() 等 |
無 | 有擴充 | 有標準函式 |
遷移時至少驗證四項:括號是否必需、$ 能否出現在過濾器裡、負步長行為、結果結構。官方一致性測試套件能覆蓋大部分語法差異,用它可以避免「換庫後少了三條資料」這類靜默錯誤。
八、效能與誤用清單
效能上只有一句話:JSONPath 是一次全量遍歷,.. 是全量遍歷裡最貴的那一種。
- 十萬節點以上的文件,先用確定的路徑縮小範圍,再對子集過濾;
- 表示式編譯一次、複用多次,別在迴圈裡反覆解析字串;
- 需要聚合(求和、分組、排序)時換
jq或程式碼,JSONPath 只負責選擇; - 條件越靠前越好,
$.store.book[?(@.price < 10)]比$..[?(@.price < 10)]少掃很多無關節點。
誤用清單:
- [ ] 把存在性判斷當成布林真值判斷;
- [ ] 用
$..代替已知層級; - [ ] 依賴指令碼表示式做跨節點比較;
- [ ] 假設結果一定是值陣列;
- [ ] 在使用者可控的表示式上沒有長度與巢狀限制。
常見問題
為什麼 @.stock 在欄位為 false 時仍然匹配?
因為單獨的路徑在過濾器裡表示「欄位存在」,不表示「值為真」。要判斷布林值必須顯式比較:@.stock == true。這是 JSONPath 與 JavaScript 真值判斷最大的語意差異。
遞迴下降的結果順序穩定嗎?
不穩定。RFC 9535 規定的是節點集語意,遍歷順序依賴實現對子節點的訪問順序。需要固定順序時,先定位陣列再按下標取值,或者拿到結果後在應用層排序。
切片和過濾器可以混用嗎?
可以,但要記住作用範圍。$.store.book[0:2][?(@.stock)] 只在切片得到的兩本書裡篩選;如果想先過濾再切片,寫成 $.store.book[?(@.stock)][0:2],順序不同結果就不同。
jsonpath-plus 支援 $.expensive,為什麼標準不允許?
因為過濾器只定義了對當前節點 @ 的訪問,跨根引用會讓表示式依賴整棵文件的上下文,難以靜態分析和跨語言移植。需要閾值時把它作為參數傳入,而不是寫進表示式。
如何避免遞迴下降掃到大量無關節點?
能確定層級時寫確定路徑,先縮小範圍再對子集過濾,不要用 $.. 代替已知層級。條件越靠前越好,$.store.book[?(@.price < 10)] 比 $..[?(@.price < 10)] 少掃很多無關節點。十萬節點以上的文件尤其要先做這一步。