JSON 工具箱

JSONPath 查詢語言實戰

結論:2024 年 JSONPath 終於有了正式標準 RFC 9535。這一篇講清語法、過濾器語義、函式,以及各實現之間的歷史差異。

進階約 12 分鐘發佈於 2026-10-30更新於 2026-10-30
JSONPath 查詢語言實戰封面

結論:先用確定的路徑定位、再疊加過濾條件,表示式按 RFC 9535 寫;只需要取固定欄位就用屬性訪問或 JSON Pointer,需要聚合就換 jq 或程式碼。

$.store.book[?(@.price < 10)].title 這種寫法你大概見過,但它成為國際標準是 2024 年的事。

一、從事實標準到正式標準

JSONPath 由 Stefan Gössner 在 2007 年提出,長期只有一篇部落格作為「規範」,各語言實現各自發揮:

  • 過濾器要不要加括號(?(@.a) vs ?@.a);
  • 陣列下標 [-1] 是否支援;
  • 結果返回「值」還是「值與路徑」;
  • 指令碼表示式(?(@.price < $.max))是否允許。

2024 年 2 月釋出的 RFC 9535 把這些分歧收斂成一套語法與語義,並給出了官方一致性測試套件。 新專案應當按 RFC 9535 來寫,老專案遷移時要先確認實現版本。

二、語法速查

語法 含義 示例
$ 根節點 $.store
@ 過濾器內的當前節點 $[?@.price < 10]
.name 子節點(名字需匹配 [A-Za-z_][A-Za-z0-9_]*) $.store.book
['name'] 子節點(任意名字,含中文、含點) $['store']['書']
[0] 陣列下標,負數從尾部數 $.book[0]、$.book[-1]
[0,2] 多個下標/多個名字 $.book[0,2]
[1:3] 切片,含頭不含尾 $.book[1:3]
[::2] 帶步長切片 $.book[::2]
[*] 通配所有子節點 $.book[*].title
..name 遞迴下降,任意深度查詢 $..price
..[*] 遞迴取全部節點 $..*
[?<expr>] 過濾選擇器 $.book[?@.price < 10]

一個必須記住的語義:JSONPath 返回的是節點集(nodelist),不是單個值。 即使只匹配一個節點,結果也是長度為一的列表。

三、用一份樣例把語法跑一遍

{
  "store": {
    "book": [
      { "title": "JSON 權威指南", "price": 8.95, "tags": ["json", "spec"] },
      { "title": "資料交換格式史", "price": 12.99, "tags": ["history"] },
      { "title": "解析器實現原理", "price": 8.99, "tags": ["parser", "json"] }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "expensive": 10
}
表示式 結果
$.store.bicycle.color ["red"]
$..price 4 個價格(含 bicycle)
$.store.book[*].title 3 個標題
$.store.book[?@.price < 10].title 第 1、3 本的標題
$.store.book[?@.tags] 所有帶 tags 的書(存在性判斷)
$.store.book[-1].title 最後一本的標題
$..book[0,1] 前兩本
$.store.book[?@.price < $.expensive] 語法上依賴實現,RFC 9535 不允許跨根引用

最後一行是歷史坑:很多實現支援在過濾器裡寫 $.expensive 做跨節點比較,RFC 9535 明確禁止—— 過濾器只能訪問當前節點 @。需要比較全域性閾值時,把閾值作為引數傳給查詢函式。

四、過濾器能寫什麼

RFC 9535 的過濾表示式支援四類構造:

邏輯:  &&  ||  !          例:?@.price < 10 && @.tags
比較:  ==  !=  <  <=  >  >=
存在性:單獨寫一個路徑     例:?@.tags(判斷存在)
函式:  length() count() match() search() value()

幾個實用例子:

// 價格在 8 到 10 之間
$.store.book[?@.price >= 8 && @.price <= 10]

// 標題包含「JSON」(正則匹配,第二個引數是 I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]

// 標籤數量大於 1
$.store.book[?length(@.tags) > 1]

// 取第一個匹配值而不是列表(value() 用於把單節點集轉成值)
$.store.book[?@.price < 9].title.value()

match() 是全串匹配、search() 是子串搜尋,底層的正則方言是 I-Regexp(RFC 9485 的子集), 不支援回溯引用與環視——這既是限制,也順手擋掉了正則 DoS。

五、實現差異:遷移前必查

實現 語言 RFC 9535 支援 備註
jsonpath-plus JavaScript 大部分相容 支援擴充套件函式與指令碼表示式,能力更強但非嚴格標準
jsonpath(dchester) JavaScript 部分 老牌庫,?() 語法,結果結構不同
ojg / gjson Go ojg 支援較好 gjson 是另一套路徑語法,不是 JSONPath
jsonpath-ng Python 部分 支援擴充套件,寫法偏寬鬆
Jayway JsonPath Java 部分 生態廣,歷史行為與 RFC 有出入

遷移檢查三項:

  1. 過濾器括號是否必需;
  2. -1 負下標是否支援;
  3. 結果是「值陣列」還是「{path, value} 物件陣列」。

第三項最容易踩:前端按 result[0] 取值,換庫後拿到的是物件,頁面直接空白。

六、效能與安全

效能

  • JSONPath 本質是一次遍歷,複雜度是 O(節點數),無法利用索引;
  • .. 遞迴下降最貴,能先 $.store.book 定位再過濾,就不要寫 $..book;
  • 表示式應先編譯再複用(多數庫提供 compile 或快取),避免每次查詢都重新解析;
  • 十萬節點以上的文件,考慮先用 JSON Pointer 精確取值,或改用 jq 在服務端預處理。

安全

  • 使用者輸入的表示式必須限制長度與巢狀層數;
  • 不要讓 JSONPath 承擔許可權控制——它只能讀,無法表達「哪些欄位不可見」;
  • match() 用的正則來自使用者時要限制複雜度,雖比 PCRE 安全,仍建議設長度上限。

七、什麼時候不該用 JSONPath

  • 只需要取一個固定欄位 → 直接 data.store.bicycle.color;
  • 需要在修改後寫回 → 用 JSON Pointer 或補丁,JSONPath 不回寫;
  • 需要聚合計算(求和、分組)→ 用 jq、SQL 或程式碼,JSONPath 只做選擇;
  • 文件超大且只需要少數字段 → 用流式解析器,別先整棵讀進記憶體。

八、檢查清單

  • [ ] 表示式遵循 RFC 9535,不依賴跨根引用;
  • [ ] 明確了結果是節點集,取單值用 [0] 或 value();
  • [ ] 表示式預編譯並複用;
  • [ ] 使用者可控的表示式有長度與複雜度限制;
  • [ ] 換實現時跑一遍官方一致性測試用例。

常見問題

如何判斷該用 JSON Pointer 還是 JSONPath?

只需要取一個固定欄位時,直接寫屬性訪問或用 JSON Pointer,不必引入表示式;需要過濾、通配或遞迴下降時再用 JSONPath。JSONPath 只負責選擇、不回寫,修改後寫回要走 JSON Pointer 或補丁。

為什麼過濾器裡不能寫 $.expensive?

RFC 9535 明確禁止跨根引用,過濾器只能訪問當前節點 @。需要比較全域性閾值時,把閾值作為引數傳給查詢函式;老實現雖然支援這種寫法,但換庫或遷移版本後會失效。

是否可以直接用 result[0] 取值?

可以,但前提是確認當前實現返回的是值陣列。部分庫返回 {path, value} 物件陣列,換庫後 result[0] 拿到的是物件,頁面會直接空白。取單值統一走 [0] 或 value(),換實現前先跑官方一致性測試用例。