JSON Toolkit

Bahasa Query JSONPath dalam Praktik

Intinya: Pada 2024 JSONPath akhirnya mendapatkan standar formal, RFC 9535. Artikel ini menjelaskan sintaks, semantik filter, fungsi, dan perbedaan historis antar implementasi.

Menengah±12 menit bacaDiterbitkan 2026-10-30Diperbarui 2026-10-30
Sampul untuk Bahasa Query JSONPath dalam Praktik

Intinya: lokasikan dengan path eksplisit terlebih dahulu dan filter di atasnya, serta tulis ekspresi sesuai RFC 9535; gunakan akses properti atau JSON Pointer untuk satu field tetap, dan jq atau kode untuk agregasi.

Anda mungkin pernah melihat ekspresi seperti $.store.book[?(@.price < 10)].title, tetapi ia baru menjadi standar internasional pada 2024.

1. Dari Standar De Facto menjadi Standar Formal

JSONPath diusulkan oleh Stefan Gössner pada 2007, dan untuk waktu lama satu-satunya "spesifikasi" adalah satu posting blog, dengan setiap implementasi bahasa melakukan caranya sendiri:

  • apakah filter memerlukan tanda kurung (?(@.a) vs ?@.a);
  • apakah indeks array [-1] didukung;
  • apakah hasil mengembalikan "nilai" atau "nilai dan path";
  • apakah ekspresi skrip (?(@.price < $.max)) diizinkan.

RFC 9535, yang diterbitkan pada Februari 2024, menyatukan divergensi ini menjadi satu sintaks dan semantik dan menyediakan compliance test suite resmi. Proyek baru harus mengikuti RFC 9535; saat memigrasi proyek lama, pastikan dulu versi implementasinya.

2. Contekan Sintaks

Sintaks Arti Contoh
$ Node akar $.store
@ Node saat ini di dalam filter $[?@.price < 10]
.name Node anak (nama harus cocok dengan [A-Za-z_][A-Za-z0-9_]*) $.store.book
['name'] Node anak (nama apa pun, termasuk non-ASCII dan titik) $['store']['书']
[0] Indeks array; angka negatif dihitung dari akhir $.book[0], $.book[-1]
[0,2] Beberapa indeks / beberapa nama $.book[0,2]
[1:3] Slice, awal inklusif dan akhir eksklusif $.book[1:3]
[::2] Slice dengan langkah $.book[::2]
[*] Wildcard untuk semua node anak $.book[*].title
..name Recursive descent, mencari pada kedalaman apa pun $..price
..[*] Secara rekursif mengambil semua node $..*
[?<expr>] Selektor filter $.book[?@.price < 10]

Satu semantik yang harus Anda ingat: JSONPath mengembalikan himpunan node (nodelist), bukan satu nilai. Bahkan ketika hanya satu node yang cocok, hasilnya adalah list dengan panjang satu.

3. Menelusuri Sintaks dengan Satu Contoh

{
  "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
}
Ekspresi Hasil
$.store.bicycle.color ["red"]
$..price 4 harga (termasuk harga sepeda)
$.store.book[*].title 3 judul
$.store.book[?@.price < 10].title Judul buku 1 dan 3
$.store.book[?@.tags] Semua buku yang memiliki tags (uji keberadaan)
$.store.book[-1].title Judul buku terakhir
$..book[0,1] Dua buku pertama
$.store.book[?@.price < $.expensive] Secara sintaks bergantung implementasi; RFC 9535 tidak mengizinkan referensi lintas akar

Baris terakhir adalah jebakan historis: banyak implementasi mengizinkan $.expensive di dalam filter untuk perbandingan lintas node, yang secara eksplisit dilarang RFC 9535 — filter hanya dapat mengakses node saat ini @. Ketika Anda perlu membandingkan dengan ambang global, lewatkan ambang sebagai argumen ke fungsi query.

4. Apa yang Dapat Diekspresikan Filter

Ekspresi filter RFC 9535 mendukung empat jenis konstruksi:

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

Beberapa contoh praktis:

// Harga antara 8 dan 10
$.store.book[?@.price >= 8 && @.price <= 10]

// Judul mengandung "JSON" (pencocokan regex; argumen kedua adalah I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]

// Lebih dari satu tag
$.store.book[?length(@.tags) > 1]

// Ambil nilai cocok pertama alih-alih list (value() mengubah himpunan node tunggal menjadi nilai)
$.store.book[?@.price < 9].title.value()

match() adalah pencocokan seluruh string dan search() adalah pencarian substring; dialek regex yang mendasari adalah I-Regexp (subset dari RFC 9485), yang tidak mendukung backreference maupun lookaround — itu sekaligus keterbatasan dan perisai yang berguna terhadap regex DoS.

5. Perbedaan Implementasi: Periksa Sebelum Migrasi

Implementasi Bahasa Dukungan RFC 9535 Catatan
jsonpath-plus JavaScript Sebagian besar kompatibel Mendukung fungsi ekstensi dan ekspresi skrip; lebih mampu tetapi tidak sepenuhnya standar
jsonpath (dchester) JavaScript Parsial Library lama dengan sintaks ?() dan struktur hasil berbeda
ojg / gjson Go ojg memiliki dukungan baik gjson menggunakan sintaks path lain dan bukan JSONPath
jsonpath-ng Python Parsial Mendukung ekstensi dan cukup permisif
Jayway JsonPath Java Parsial Ekosistem luas; perilaku historis berbeda dari RFC

Tiga pemeriksaan sebelum migrasi:

  1. Apakah tanda kurung filter wajib;
  2. Apakah indeks negatif seperti -1 didukung;
  3. Apakah hasilnya "array nilai" atau "array objek {path, value}".

Yang ketiga paling mudah tersandung: frontend membaca result[0], dan setelah berganti library ia menerima objek, membuat halaman kosong.

6. Performa dan Keamanan

Performa

  • JSONPath pada dasarnya adalah satu traversal dengan kompleksitas O(jumlah node), dan ia tidak dapat menggunakan indeks;
  • Recursive descent dengan .. adalah yang paling mahal; jika Anda dapat melokasikan dengan $.store.book dulu lalu memfilter, jangan menulis $..book;
  • Kompilasi ekspresi sekali dan gunakan kembali (sebagian besar library menawarkan compile atau caching) alih-alih mengurai ulang pada setiap query;
  • Untuk dokumen dengan lebih dari 100.000 node, pertimbangkan ekstraksi tepat dengan JSON Pointer terlebih dahulu, atau prapemrosesan di server dengan jq.

Keamanan

  • Ekspresi dari masukan pengguna harus memiliki batas panjang dan kedalaman penyarangan;
  • Jangan biarkan JSONPath menangani otorisasi — ia hanya dapat membaca, dan tidak dapat mengekspresikan "field mana yang tidak terlihat";
  • Ketika regex yang digunakan match() berasal dari pengguna, batasi kompleksitasnya; meskipun lebih aman daripada PCRE, batas panjang tetap disarankan.

7. Kapan Tidak Menggunakan JSONPath

  • Anda hanya membutuhkan satu field tetap → gunakan data.store.bicycle.color secara langsung;
  • Anda perlu menulis balik setelah memodifikasi → gunakan JSON Pointer atau patch; JSONPath tidak menulis balik;
  • Anda memerlukan agregasi (penjumlahan, pengelompokan) → gunakan jq, SQL, atau kode; JSONPath hanya memilih;
  • Dokumennya sangat besar dan Anda hanya membutuhkan beberapa field → gunakan parser streaming alih-alih membaca seluruh pohon ke memori terlebih dahulu.

8. Daftar Periksa

  • [ ] Ekspresi mengikuti RFC 9535 dan tidak bergantung pada referensi lintas akar;
  • [ ] Jelas bahwa hasilnya adalah himpunan node; gunakan [0] atau value() untuk mendapatkan satu nilai;
  • [ ] Ekspresi dikompilasi terlebih dahulu dan digunakan kembali;
  • [ ] Ekspresi yang dikendalikan pengguna memiliki batas panjang dan kompleksitas;
  • [ ] Kasus uji compliance resmi dijalankan saat berganti implementasi.

FAQ

Bagaimana saya memilih antara JSON Pointer dan JSONPath?

Gunakan akses properti biasa atau JSON Pointer ketika Anda hanya membutuhkan satu field tetap; gunakan JSONPath ketika Anda memerlukan filter, wildcard, atau recursive descent. JSONPath hanya memilih dan tidak pernah menulis balik, jadi penulisan balik milik JSON Pointer atau patch.

Mengapa filter tidak dapat merujuk $.expensive?

RFC 9535 secara eksplisit melarang referensi lintas akar; filter hanya boleh mengakses node saat ini @. Lewatkan ambang sebagai argumen ke fungsi query sebagai gantinya. Implementasi lama menerima sintaks ini, tetapi ia rusak ketika Anda berganti atau meningkatkan library.

Haruskah saya mengandalkan result[0] untuk membaca nilai?

Hanya setelah memastikan bahwa implementasi saat ini mengembalikan array nilai. Beberapa library mengembalikan objek {path, value}, sehingga result[0] menjadi objek dan halaman menjadi kosong. Baca nilai tunggal melalui [0] atau value(), dan jalankan compliance suite resmi sebelum berganti implementasi.