JSON Toolkit

Filter JSONPath chuyên sâu: đệ quy, slice và khớp nhiều điều kiện

Kết luận: Duyệt đệ quy, slice và filter nhiều điều kiện là chỗ JSONPath mạnh lên và cũng bị dùng sai nhiều nhất. Bài viết nói về ngữ nghĩa filter, hình dạng kết quả và khác biệt thư viện.

Trung cấpĐọc khoảng 10 phútXuất bản 2026-10-31Cập nhật 2026-10-31
Ảnh bìa Filter JSONPath chuyên sâu

Tóm lại: hãy định vị một độ sâu đã biết trước, rồi mới thêm slice và filter thay vì vươn tới $..; một đường dẫn trần trong filter chỉ thử tồn tại, nên hãy thử boolean bằng @.stock == true.

Ai cũng viết được $.store.book[0].title. Thứ thật sự làm hỏng JSONPath là kết hợp $.. với slice và nhiều điều kiện, rồi nhận quá nhiều kết quả, quá ít kết quả, hoặc một thứ tự không mong đợi.

1. Một tài liệu mẫu

{
  "store": {
    "book": [
      { "title": "JSON: The Definitive Guide", "price": 8.95, "stock": true, "tags": ["json", "spec"] },
      { "title": "A History of Data Formats", "price": 12.99, "stock": false, "tags": ["history"] },
      { "title": "How Parsers Work", "price": 8.99, "stock": true, "tags": ["parser", "json"] },
      { "title": "Streaming JSON in Practice", "price": 9.5, "stock": true, "tags": [] }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "updatedAt": "2026-10-31"
}

Mọi biểu thức dưới đây dùng tài liệu này làm đầu vào. Hãy nhớ hai tiền đề: kết quả là một tập nút, và thứ tự của nó theo thứ tự tài liệu chứ không theo thứ tự viết điều kiện. Bên trong filter, @ chỉ tham chiếu nút ứng viên hiện tại.

2. Duyệt đệ quy $..: tiện nhưng tốn

.. nghĩa là "tìm đệ quy ở mọi độ sâu từ vị trí hiện tại". RFC 9535 định nghĩa nó là một đoạn con cháu, và nó có thể kết hợp với tên, wildcard, chỉ số, slice và filter:

$..price                  // every member named price at any depth
$..book[?(@.price < 10)]  // any book at any depth, then filter
$..[?(@.stock)]           // any node at any depth where stock exists

Ba điều cần hiểu:

  1. $..book tìm mọi nút tên book, bất kể độ sâu;
  2. Việc duyệt theo chiều sâu trước, và thứ tự đầu ra phụ thuộc cách triển khai ghé thăm các nút con;
  3. .. không dùng được chỉ số nào, nên chi phí của nó tỷ lệ với tổng số nút.

Kết quả là $..price và $.store.book[*].price có thể chỉ khác nhau ở giá xe đạp trong đầu ra, nhưng chênh nhau một bậc về chi phí. Khi biết độ sâu, hãy luôn viết đường dẫn tường minh. Nếu bạn chỉ muốn so sánh ranh giới của hai ngôn ngữ đường dẫn, xem JSONPath so với JSON Pointer.

3. Slice mảng và wildcard: ba cái bẫy

Cú pháp slice là [start:end:step], bao gồm điểm đầu và loại trừ điểm cuối, với số âm đếm từ đuôi:

Biểu thức Ý nghĩa Kết quả (với bốn cuốn sách)
[0:2] Hai cuốn đầu Cuốn 1 và 2
[1:] Từ cuốn thứ hai đến hết Cuốn 2, 3 và 4
[:2] Hai cuốn đầu Cuốn 1 và 2
[-2:] Hai cuốn cuối Cuốn 3 và 4
[::2] Cách một cuốn Cuốn 1 và 3
[::-1] Đảo thứ tự Cuốn 4, 3, 2 và 1

Ba cái bẫy:

  • Bước bằng không không hợp lệ. RFC 9535 yêu cầu bước khác không, trong khi một số triển khai âm thầm trả về tập rỗng;
  • Bước âm không được hỗ trợ ở mọi nơi. Triển khai gốc của Goessner hỗ trợ, nhưng một số triển khai tối giản chỉ nhận bước dương;
  • Lọc sau slice không mở rộng phạm vi. [0:2][?(@.stock)] chỉ lọc hai cuốn do slice tạo ra; nó không quay lại toàn mảng.

Wildcard [*] có thể kết hợp với slice, nhưng [*] tạo ra "mọi nút con". Nếu các nút con đó là object, gắn thêm .title sẽ cho mọi tiêu đề. Nếu một nút con bản thân là mảng, .title sẽ không tự động làm phẳng thêm một cấp. Hãy viết [*] lần nữa khi cần mở rộng; đừng mong đợi việc làm phẳng ngầm.

4. Filter nhiều điều kiện: so sánh, logic và tồn tại

Một filter [?(...)] có thể chứa bốn loại cấu trúc: so sánh, toán tử logic, phép thử tồn tại và hàm. Một truy vấn nhiều điều kiện thường gặp trông như sau:

$..book[?(@.price < 10 && @.stock)]

Nó nghĩa là "giá dưới 10 và stock là truthy". Hãy chú ý vài chi tiết ngữ nghĩa:

  • @.stock là phép thử tồn tại, không phải "giá trị là true". Nếu trường tồn tại nhưng giá trị là false, điều kiện vẫn đúng. Để thử một giá trị boolean, bạn phải viết @.stock == true;
  • && kết hợp chặt hơn ||. Hãy thêm ngoặc khi trộn chúng thay vì dựa vào trí nhớ;
  • Hai phía của một so sánh phải cùng kiểu. Số và chuỗi không được chuyển đổi ngầm; @.price < "10" là sai hoặc lỗi trong các triển khai nghiêm ngặt;
  • Một trường vắng mặt luôn làm so sánh sai. @.missing < 10 không báo lỗi; nó chỉ không khớp gì.

Đây là chỗ dễ vấp nhất khi viết filter như thể viết JavaScript: if (obj.stock) thử truthiness trong JavaScript, còn JSONPath thử tồn tại. Về cú pháp chuẩn và các hàm đầy đủ, xem lại Ngôn ngữ truy vấn JSONPath trong thực tế.

5. Biểu thức script: mạnh hơn, ít khả chuyển hơn

Triển khai gốc của Goessner và jsonpath-plus cho phép so sánh chéo nút như [?(@.price < $.max)], và thậm chí cả script tuỳ ý. RFC 9535 chỉ cho phép truy cập nút hiện tại @ và cấm $ bên trong filter.

// Works in older implementations; not allowed by RFC 9535
$..book[?(@.price < $.expensive)]

// Standard approach: pass the threshold from the caller
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })

Biểu thức script tạo ra ba vấn đề:

  1. Khả chuyển về không. Đổi thư viện nghĩa là viết lại mọi biểu thức;
  2. Ranh giới bảo mật biến mất. Khi biểu thức đến từ người dùng, việc đánh giá script trao luôn khả năng thực thi mã;
  3. Tối ưu tĩnh trở nên bất khả. Triển khai chỉ có thể diễn giải từng nút, chậm và khó cache.

Nếu bạn thật sự cần so sánh chéo nút, hãy truyền ngưỡng làm tham số truy vấn và lọc lần hai trong mã ứng dụng. Cách đó thường ổn định hơn là phụ thuộc vào cú pháp mở rộng.

6. Hình dạng kết quả: bạn tưởng nhận giá trị, nhưng nhận nút

Cùng một biểu thức có thể trả về ít nhất ba hình dạng giữa các thư viện:

["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // value array
[{ "path": "$['store']['book'][0]['title']", "value": "..." }]                 // path + value
[{ "pointer": "/store/book/0/title", "value": "..." }]                          // JSON Pointer + value

Điều này ảnh hưởng trực tiếp đến ba việc: cách frontend đọc kết quả, cách khử trùng lặp kết quả, và cách kết quả truy vấn trở thành thao tác ghi. Nodelist mà RFC 9535 định nghĩa là một tập tham chiếu nút, nhưng triển khai JavaScript thường chỉ trả giá trị vì tham chiếu không vượt qua được ranh giới tiến trình.

Lời khuyên thực tế:

  • Luôn đọc một giá trị đơn qua [0] hoặc value(); đừng bao giờ cho rằng kết quả là vô hướng;
  • Khi cần ghi lại, chọn triển khai trả về JSON Pointer hoặc tự dựng pointer;
  • Kết quả rỗng và kết quả [null] là khác nhau; hãy kiểm tra độ dài thay vì truthiness.

7. Khác biệt thư viện: chạy bộ kiểm thử tuân thủ trước khi di trú

Năng lực Goessner gốc jsonpath-plus RFC 9535 nghiêm ngặt
Ngoặc filter ?() Bắt buộc Tương thích Tuỳ chọn
Tham chiếu chéo gốc $.x Được hỗ trợ Được hỗ trợ Bị cấm
Biểu thức script Được hỗ trợ Được hỗ trợ Bị cấm
Chỉ số và bước âm Được hỗ trợ Được hỗ trợ Được hỗ trợ
Trả về đường dẫn Cấu hình được Cấu hình được Không quy định
Hàm như length() Không có Mở rộng Hàm chuẩn

Trước khi di trú, hãy xác minh ít nhất bốn điều: ngoặc có bắt buộc không, $ có được xuất hiện trong filter không, bước âm hành xử thế nào, và cấu trúc kết quả. Bộ kiểm thử tuân thủ chính thức bao phủ phần lớn khác biệt cú pháp và ngăn những thất bại âm thầm kiểu "ba dòng biến mất sau khi đổi thư viện".

8. Hiệu năng và danh sách kiểm tra lạm dụng

Hiệu năng gói lại trong một câu: JSONPath là một lần duyệt toàn bộ, và .. là kiểu duyệt toàn bộ tốn nhất.

  • Với tài liệu hơn 100.000 nút, hãy thu hẹp phạm vi bằng một đường dẫn tường minh trước, rồi lọc tập con;
  • Biên dịch biểu thức một lần và tái sử dụng; đừng phân tích lại chuỗi bên trong vòng lặp;
  • Chuyển sang jq hoặc mã cho việc tổng hợp (tính tổng, nhóm, sắp xếp); JSONPath chỉ chọn;
  • Đặt điều kiện càng sớm càng tốt: $.store.book[?(@.price < 10)] quét ít nút không liên quan hơn nhiều so với $..[?(@.price < 10)].

Danh sách kiểm tra lạm dụng:

  • [ ] Coi phép thử tồn tại là phép thử truthiness boolean;
  • [ ] Dùng $.. thay cho một độ sâu đã biết;
  • [ ] Phụ thuộc biểu thức script để so sánh chéo nút;
  • [ ] Cho rằng kết quả luôn là mảng giá trị;
  • [ ] Cho phép biểu thức do người dùng kiểm soát mà không giới hạn độ dài và độ sâu lồng.

FAQ

Vì sao @.stock vẫn khớp khi trường là false?

Vì một đường dẫn trần trong filter nghĩa là "trường tồn tại", không phải "giá trị là true". Để thử boolean, hãy so sánh tường minh với @.stock == true. Đây là khác biệt ngữ nghĩa lớn nhất giữa JSONPath và truthiness của JavaScript.

Thứ tự duyệt đệ quy có ổn định không?

Không. RFC 9535 quy định ngữ nghĩa tập nút, và thứ tự duyệt phụ thuộc cách triển khai ghé thăm các nút con. Khi cần thứ tự cố định, hãy định vị mảng và đọc theo chỉ số, hoặc sắp xếp kết quả trong mã ứng dụng.

Có thể kết hợp slice và filter không?

Có, nhưng hãy nhớ phạm vi. $.store.book[0:2][?(@.stock)] chỉ lọc hai cuốn do slice tạo ra. Để lọc trước rồi slice sau, hãy viết $.store.book[?(@.stock)][0:2]; thứ tự khác cho kết quả khác.

Vì sao jsonpath-plus hỗ trợ $.expensive trong khi chuẩn cấm?

Vì filter chỉ được định nghĩa theo nút hiện tại @. Một tham chiếu chéo gốc khiến biểu thức phụ thuộc vào ngữ cảnh toàn tài liệu, khó phân tích tĩnh và khó chuyển giữa các ngôn ngữ. Hãy truyền ngưỡng làm tham số thay vì nhúng nó vào biểu thức.

Làm sao để duyệt đệ quy không quét các nút không liên quan?

Hãy viết đường dẫn tường minh mỗi khi biết độ sâu, thu hẹp phạm vi trước, và chỉ lọc tập con thay vì dùng $.. thay cho một cấp đã biết. Đặt điều kiện càng sớm càng tốt: $.store.book[?(@.price < 10)] quét ít nút không liên quan hơn nhiều so với $..[?(@.price < 10)]. Với tài liệu hơn 100.000 nút, hãy làm điều này trước.