JSON Toolkit

Ngôn ngữ truy vấn JSONPath trong thực tế

Kết luận: Năm 2024 JSONPath cuối cùng có một chuẩn chính thức, RFC 9535. Bài viết giải thích cú pháp, ngữ nghĩa filter, các hàm, và những khác biệt lịch sử giữa các triển khai.

Trung cấpĐọc khoảng 12 phútXuất bản 2026-10-30Cập nhật 2026-10-30
Ảnh bìa Ngôn ngữ truy vấn JSONPath trong thực tế

Tóm lại: hãy định vị bằng một đường dẫn tường minh trước rồi lọc trên nền đó, và viết biểu thức theo RFC 9535; dùng truy cập thuộc tính hoặc JSON Pointer cho một trường cố định, và dùng jq hoặc mã cho việc tổng hợp.

Chắc bạn đã thấy những biểu thức như $.store.book[?(@.price < 10)].title, nhưng nó chỉ trở thành chuẩn quốc tế vào năm 2024.

1. Từ chuẩn thực tế đến chuẩn chính thức

JSONPath được Stefan Gössner đề xuất năm 2007, và trong thời gian dài "đặc tả" duy nhất chỉ là một bài blog, với mỗi triển khai ngôn ngữ làm theo cách riêng:

  • filter có cần ngoặc hay không (?(@.a) so với ?@.a);
  • chỉ số mảng âm [-1] có được hỗ trợ không;
  • kết quả trả về "giá trị" hay "giá trị và đường dẫn";
  • có cho phép biểu thức script (?(@.price < $.max)) hay không.

RFC 9535, phát hành tháng 2 năm 2024, quy tụ những khác biệt này thành một cú pháp và ngữ nghĩa duy nhất và cung cấp một bộ kiểm thử tuân thủ chính thức. Dự án mới nên theo RFC 9535; khi di trú dự án cũ, hãy xác nhận phiên bản triển khai trước.

2. Bảng tóm tắt cú pháp

Cú pháp Ý nghĩa Ví dụ
$ Nút gốc $.store
@ Nút hiện tại bên trong filter $[?@.price < 10]
.name Nút con (tên phải khớp [A-Za-z_][A-Za-z0-9_]*) $.store.book
['name'] Nút con (tên bất kỳ, kể cả không phải ASCII và có dấu chấm) $['store']['书']
[0] Chỉ số mảng; số âm đếm từ cuối $.book[0], $.book[-1]
[0,2] Nhiều chỉ số / nhiều tên $.book[0,2]
[1:3] Slice, bao gồm điểm đầu và loại trừ điểm cuối $.book[1:3]
[::2] Slice có bước $.book[::2]
[*] Wildcard cho mọi nút con $.book[*].title
..name Duyệt đệ quy, tìm ở mọi độ sâu $..price
..[*] Lấy đệ quy mọi nút $..*
[?<expr>] Bộ chọn filter $.book[?@.price < 10]

Một ngữ nghĩa bạn phải nhớ: JSONPath trả về một tập nút (nodelist), không phải một giá trị duy nhất. Kể cả khi chỉ một nút khớp, kết quả cũng là một danh sách có độ dài một.

3. Chạy qua cú pháp bằng một mẫu

{
  "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
}
Biểu thức Kết quả
$.store.bicycle.color ["red"]
$..price 4 giá (kể cả giá xe đạp)
$.store.book[*].title 3 tiêu đề
$.store.book[?@.price < 10].title Tiêu đề của cuốn 1 và cuốn 3
$.store.book[?@.tags] Mọi cuốn sách có tags (phép thử tồn tại)
$.store.book[-1].title Tiêu đề cuốn cuối
$..book[0,1] Hai cuốn đầu
$.store.book[?@.price < $.expensive] Về cú pháp phụ thuộc triển khai; RFC 9535 không cho phép tham chiếu chéo gốc

Dòng cuối là một cái bẫy lịch sử: nhiều triển khai cho phép $.expensive bên trong filter để so sánh chéo nút, điều mà RFC 9535 cấm rõ ràng — một filter chỉ có thể truy cập nút hiện tại @. Khi cần so sánh với một ngưỡng toàn cục, hãy truyền ngưỡng đó làm tham số cho hàm truy vấn.

4. Filter có thể biểu đạt những gì

Biểu thức filter của RFC 9535 hỗ trợ bốn loại cấu trúc:

Logic:       &&  ||  !          e.g. ?@.price < 10 && @.tags
Comparison:  ==  !=  <  <=  >  >=
Existence:   a bare path        e.g. ?@.tags (tests existence)
Functions:   length() count() match() search() value()

Vài ví dụ thực tế:

// Price between 8 and 10
$.store.book[?@.price >= 8 && @.price <= 10]

// Title contains "JSON" (regex match; the second argument is an I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]

// More than one tag
$.store.book[?length(@.tags) > 1]

// Get the first matched value instead of a list (value() turns a single-node set into a value)
$.store.book[?@.price < 9].title.value()

match() là khớp toàn chuỗi còn search() là tìm chuỗi con; phương ngữ regex bên dưới là I-Regexp (một tập con của RFC 9485), vốn không hỗ trợ tham chiếu ngược cũng không hỗ trợ lookaround — đó vừa là hạn chế vừa là tấm khiên tiện lợi chống DoS regex.

5. Khác biệt giữa các triển khai: kiểm tra trước khi di trú

Triển khai Ngôn ngữ Hỗ trợ RFC 9535 Ghi chú
jsonpath-plus JavaScript Phần lớn tương thích Hỗ trợ hàm mở rộng và biểu thức script; mạnh hơn nhưng không thật sự chuẩn
jsonpath (dchester) JavaScript Một phần Thư viện lâu đời với cú pháp ?() và cấu trúc kết quả khác
ojg / gjson Go ojg hỗ trợ tốt gjson dùng cú pháp đường dẫn khác và không phải JSONPath
jsonpath-ng Python Một phần Hỗ trợ mở rộng và khá dễ dãi
Jayway JsonPath Java Một phần Hệ sinh thái rộng; hành vi lịch sử khác RFC

Ba điều cần kiểm tra trước khi di trú:

  1. Filter có bắt buộc ngoặc hay không;
  2. Chỉ số âm như -1 có được hỗ trợ hay không;
  3. Kết quả là "mảng giá trị" hay "mảng object {path, value}".

Điều thứ ba dễ vấp nhất: frontend đọc result[0], và sau khi đổi thư viện nó nhận một object, khiến trang trắng.

6. Hiệu năng và bảo mật

Hiệu năng

  • JSONPath về bản chất là một lần duyệt với độ phức tạp O(số nút), và nó không thể dùng chỉ mục;
  • Duyệt đệ quy bằng .. là tốn nhất; nếu có thể định vị bằng $.store.book trước rồi lọc, đừng viết $..book;
  • Biên dịch biểu thức một lần và tái sử dụng (hầu hết thư viện cung cấp compile hoặc cache) thay vì phân tích lại mỗi truy vấn;
  • Với tài liệu hơn 100.000 nút, cân nhắc trích xuất chính xác bằng JSON Pointer trước, hoặc tiền xử lý trên server bằng jq.

Bảo mật

  • Biểu thức từ đầu vào người dùng phải có giới hạn độ dài và độ sâu lồng;
  • Đừng để JSONPath xử lý phân quyền — nó chỉ đọc được, và không biểu đạt được "trường nào không được thấy";
  • Khi regex mà match() dùng đến từ người dùng, hãy giới hạn độ phức tạp của nó; dù an toàn hơn PCRE, vẫn nên đặt giới hạn độ dài.

7. Khi nào không nên dùng JSONPath

  • Bạn chỉ cần một trường cố định → dùng thẳng data.store.bicycle.color;
  • Bạn cần ghi lại sau khi sửa → dùng JSON Pointer hoặc patch; JSONPath không ghi lại;
  • Bạn cần tổng hợp (tính tổng, nhóm) → dùng jq, SQL hoặc mã; JSONPath chỉ chọn;
  • Tài liệu rất lớn và bạn chỉ cần vài trường → dùng bộ phân tích streaming thay vì đọc cả cây vào bộ nhớ trước.

8. Danh sách kiểm tra

  • [ ] Biểu thức theo RFC 9535 và không dựa vào tham chiếu chéo gốc;
  • [ ] Rõ ràng rằng kết quả là một tập nút; dùng [0] hoặc value() để lấy một giá trị;
  • [ ] Biểu thức được biên dịch trước và tái sử dụng;
  • [ ] Biểu thức do người dùng kiểm soát có giới hạn độ dài và độ phức tạp;
  • [ ] Chạy các ca kiểm thử tuân thủ chính thức khi đổi triển khai.

FAQ

Làm sao chọn giữa JSON Pointer và JSONPath?

Dùng truy cập thuộc tính thường hoặc JSON Pointer khi bạn chỉ cần một trường cố định; chuyển sang JSONPath khi cần filter, wildcard hoặc duyệt đệ quy. JSONPath chỉ chọn và không bao giờ ghi lại, nên phần ghi lại thuộc về JSON Pointer hoặc patch.

Vì sao filter không thể tham chiếu $.expensive?

RFC 9535 cấm rõ ràng tham chiếu chéo gốc; một filter chỉ được truy cập nút hiện tại @. Thay vào đó, hãy truyền ngưỡng làm tham số cho hàm truy vấn. Các triển khai cũ chấp nhận cú pháp này, nhưng nó hỏng khi bạn đổi hay nâng cấp thư viện.

Có nên dựa vào result[0] để đọc một giá trị?

Chỉ sau khi đã xác nhận triển khai hiện tại trả về mảng giá trị. Một số thư viện trả về object {path, value}, nên result[0] thành một object và trang trắng. Hãy đọc giá trị đơn qua [0] hoặc value(), và chạy bộ kiểm thử tuân thủ chính thức trước khi đổi triển khai.

Tài liệu tham khảo

Công cụ phù hợp

Bài viết liên quan

JSON Pointer, Patch và Merge Patch

Ba đặc tả ngắn tạo thành một giao thức thay đổi: Pointer định vị, Patch sửa chính xác, Merge Patch hợp nhất từng phần. Chọn sai sẽ làm đau mảng và điều kiện đồng thời.

Trung cấpĐọc khoảng 12 phútCập nhật 2026-10-30

So sánh thư viện phân tích và xử lý JSON

API gốc nhanh nhất, parser dung sai cho offset. Hãy chọn theo ba điều: định vị lỗi, có giữ văn bản gốc không, và ranh giới có tường minh không.

Trung cấpĐọc khoảng 11 phútCập nhật 2026-10-30