Filter JSONPath Secara Mendalam: Rekursi, Slice, dan Pencocokan Multi-Kondisi
Intinya: Recursive descent, slice, dan filter multi-kondisi adalah tempat JSONPath menjadi kuat sekaligus disalahgunakan. Mencakup semantik filter, bentuk hasil, dan perbedaan library.

Intinya: lokasikan dulu kedalaman yang diketahui, lalu tambahkan slice dan filter alih-alih menggunakan $..; path polos di dalam filter hanya menguji keberadaan, jadi uji boolean dengan @.stock == true.
Siapa pun bisa menulis
$.store.book[0].title. Yang sebenarnya merusak JSONPath adalah menggabungkan$..dengan slice dan beberapa kondisi, lalu mendapatkan terlalu banyak hasil, terlalu sedikit hasil, atau urutan yang tidak Anda duga.
1. Dokumen Contoh
{
"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"
}
Setiap ekspresi di bawah menggunakan dokumen ini sebagai masukan. Ingat dua premis: hasilnya adalah himpunan node, dan urutannya mengikuti urutan dokumen bukan urutan penulisan kondisi. Di dalam filter, @ hanya merujuk node kandidat saat ini.
2. Recursive Descent $..: Nyaman tetapi Mahal
.. berarti "cari secara rekursif pada kedalaman apa pun dari posisi saat ini". RFC 9535 mendefinisikannya sebagai segmen keturunan, dan ia dapat digabungkan dengan nama, wildcard, indeks, slice, dan 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
Tiga hal yang perlu dipahami:
$..bookmenemukan setiap node bernamabook, terlepas dari kedalamannya;- Traversal bersifat depth-first, dan urutan keluaran bergantung pada cara implementasi mengunjungi anak;
..tidak dapat menggunakan indeks apa pun, sehingga biayanya berskala dengan jumlah total node.
Akibatnya, $..price dan $.store.book[*].price mungkin hanya berbeda pada harga sepeda dalam keluarannya, tetapi berbeda satu orde besaran dalam biaya. Ketika Anda mengetahui kedalaman, selalu tulis path eksplisit. Jika Anda hanya ingin membandingkan batas kedua bahasa path, lihat JSONPath vs JSON Pointer.
3. Slice Array dan Wildcard: Tiga Jebakan
Sintaks slice adalah [start:end:step], inklusif pada awal dan eksklusif pada akhir, dengan angka negatif dihitung dari ekor:
| Ekspresi | Arti | Hasil (dengan empat buku) |
|---|---|---|
[0:2] |
Dua buku pertama | Buku 1 dan 2 |
[1:] |
Dari buku kedua hingga akhir | Buku 2, 3, dan 4 |
[:2] |
Dua buku pertama | Buku 1 dan 2 |
[-2:] |
Dua buku terakhir | Buku 3 dan 4 |
[::2] |
Setiap buku kedua | Buku 1 dan 3 |
[::-1] |
Urutan terbalik | Buku 4, 3, 2, dan 1 |
Tiga jebakan:
- Langkah nol tidak valid. RFC 9535 mensyaratkan langkah bukan nol, sementara beberapa implementasi diam-diam mengembalikan himpunan kosong;
- Langkah negatif tidak didukung secara universal. Implementasi Goessner asli mendukungnya, tetapi beberapa implementasi minimal hanya menerima langkah positif;
- Memfilter setelah slice tidak memperluas cakupan.
[0:2][?(@.stock)]hanya memfilter dua buku yang dihasilkan slice; ia tidak kembali ke seluruh array.
Wildcard [*] dapat digabungkan dengan slice, tetapi [*] menghasilkan "semua node anak". Jika anak-anak itu adalah objek, menambahkan .title memberikan semua judul. Jika seorang anak itu sendiri adalah array, .title tidak akan secara implisit meratakan level lain. Tulis [*] lagi ketika Anda perlu memperluas; jangan mengharapkan perataan implisit.
4. Filter Multi-Kondisi: Perbandingan, Logika, dan Keberadaan
Filter [?(...)] dapat berisi empat jenis konstruksi: perbandingan, operator logika, uji keberadaan, dan fungsi. Query multi-kondisi yang umum terlihat seperti ini:
$..book[?(@.price < 10 && @.stock)]
Ini berarti "harga di bawah 10 dan stok bernilai truthy". Perhatikan beberapa detail semantik:
@.stockadalah uji keberadaan, bukan "nilainya true". Jika field ada tetapi nilainyafalse, kondisi tetap berlaku. Untuk menguji nilai boolean Anda harus menulis@.stock == true;&&mengikat lebih kuat daripada||. Tambahkan tanda kurung saat mencampurnya alih-alih mengandalkan ingatan;- Kedua sisi perbandingan harus memiliki tipe yang sama. Angka dan string tidak dikonversi secara implisit;
@.price < "10"bernilai false atau error pada implementasi ketat; - Field yang hilang selalu membuat perbandingan bernilai false.
@.missing < 10tidak menimbulkan error; ia hanya tidak mencocokkan apa pun.
Ini adalah tempat termudah untuk tersandung ketika menulis filter seolah-olah itu JavaScript: if (obj.stock) menguji truthiness di JavaScript, sedangkan JSONPath menguji keberadaan. Untuk sintaks dan fungsi standar lengkap, tinjau kembali Bahasa Query JSONPath dalam Praktik.
5. Ekspresi Skrip: Lebih Banyak Kekuatan, Lebih Sedikit Portabilitas
Implementasi Goessner asli dan jsonpath-plus mengizinkan perbandingan lintas node seperti [?(@.price < $.max)], bahkan skrip arbitrer. RFC 9535 secara eksplisit hanya mengizinkan akses ke node saat ini @ dan melarang $ di dalam 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 })
Ekspresi skrip menciptakan tiga masalah:
- Portabilitas turun menjadi nol. Berganti library berarti menulis ulang setiap ekspresi;
- Batas keamanan hilang. Ketika ekspresi berasal dari pengguna, evaluasi skrip menyerahkan eksekusi kode;
- Optimasi statis menjadi mustahil. Implementasi hanya dapat menafsirkan node demi node, yang lambat dan sulit di-cache.
Jika Anda benar-benar memerlukan perbandingan lintas node, lewatkan ambang sebagai parameter query dan filter untuk kedua kalinya di kode aplikasi. Itu biasanya lebih stabil daripada bergantung pada sintaks ekstensi.
6. Bentuk Hasil: Anda Mengira Mendapat Nilai, tetapi Mendapat Node
Ekspresi yang sama dapat mengembalikan setidaknya tiga bentuk di berbagai library:
["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
Ini secara langsung memengaruhi tiga hal: cara frontend membaca hasil, cara hasil dideduplikasi, dan cara hasil query menjadi operasi tulis. Nodelist yang didefinisikan RFC 9535 adalah himpunan referensi node, tetapi implementasi JavaScript biasanya hanya mengembalikan nilai karena referensi tidak dapat melintasi batas proses.
Saran praktis:
- Selalu baca satu nilai melalui
[0]atauvalue(); jangan pernah mengasumsikan hasilnya adalah skalar; - Ketika Anda perlu menulis balik, pilih implementasi yang mengembalikan JSON Pointer atau bangun pointer sendiri;
- Hasil kosong dan hasil
[null]berbeda; periksa panjang alih-alih truthiness.
7. Perbedaan Library: Jalankan Compliance Suite Sebelum Migrasi
| Kemampuan | Goessner Asli | jsonpath-plus | RFC 9535 Ketat |
|---|---|---|---|
Tanda kurung filter ?() |
Wajib | Kompatibel | Opsional |
Referensi lintas akar $.x |
Didukung | Didukung | Dilarang |
| Ekspresi skrip | Didukung | Didukung | Dilarang |
| Indeks dan langkah negatif | Didukung | Didukung | Didukung |
| Mengembalikan path | Dapat dikonfigurasi | Dapat dikonfigurasi | Tidak ditentukan |
Fungsi seperti length() |
Tidak ada | Ekstensi | Fungsi standar |
Sebelum migrasi, pastikan setidaknya empat hal: apakah tanda kurung wajib, apakah $ boleh muncul di dalam filter, bagaimana perilaku langkah negatif, dan struktur hasil. Compliance test suite resmi mencakup sebagian besar perbedaan sintaks dan mencegah kegagalan senyap seperti "tiga baris menghilang setelah berganti library".
8. Daftar Periksa Performa dan Penyalahgunaan
Performa bermuara pada satu kalimat: JSONPath adalah traversal penuh, dan .. adalah jenis traversal penuh yang paling mahal.
- Untuk dokumen dengan lebih dari 100.000 node, sempitkan cakupan dengan path eksplisit terlebih dahulu, lalu filter subsetnya;
- Kompilasi ekspresi sekali dan gunakan kembali; jangan mengurai ulang string di dalam loop;
- Beralih ke
jqatau kode untuk agregasi (penjumlahan, pengelompokan, pengurutan); JSONPath hanya memilih; - Letakkan kondisi sedini mungkin:
$.store.book[?(@.price < 10)]memindai jauh lebih sedikit node tidak relevan daripada$..[?(@.price < 10)].
Daftar periksa penyalahgunaan:
- [ ] Menganggap uji keberadaan sebagai uji truth boolean;
- [ ] Menggunakan
$..alih-alih kedalaman yang diketahui; - [ ] Bergantung pada ekspresi skrip untuk perbandingan lintas node;
- [ ] Mengasumsikan hasilnya selalu array nilai;
- [ ] Mengizinkan ekspresi yang dikendalikan pengguna tanpa batas panjang dan penyarangan.
FAQ
Mengapa @.stock tetap cocok ketika field-nya false?
Karena path polos di dalam filter berarti "field itu ada", bukan "nilainya true". Untuk menguji boolean, bandingkan secara eksplisit dengan @.stock == true. Ini adalah perbedaan semantik terbesar antara JSONPath dan truthiness JavaScript.
Apakah urutan recursive descent stabil?
Tidak. RFC 9535 menetapkan semantik himpunan node, dan urutan traversal bergantung pada cara implementasi mengunjungi anak. Ketika Anda memerlukan urutan tetap, lokasikan array dan baca berdasarkan indeks, atau urutkan hasilnya di kode aplikasi.
Dapatkah slice dan filter digabungkan?
Ya, tetapi ingat cakupannya. $.store.book[0:2][?(@.stock)] hanya memfilter dua buku yang dihasilkan slice. Untuk memfilter dulu lalu slice, tulis $.store.book[?(@.stock)][0:2]; urutan berbeda memberikan hasil berbeda.
Mengapa jsonpath-plus mendukung $.expensive padahal standar melarangnya?
Karena filter didefinisikan hanya dalam hal node saat ini @. Referensi lintas akar membuat ekspresi bergantung pada konteks seluruh dokumen, yang sulit dianalisis secara statis dan diporting antar bahasa. Lewatkan ambang sebagai parameter alih-alih menyematkannya dalam ekspresi.
Bagaimana saya mencegah recursive descent memindai node tidak relevan?
Tulis path eksplisit setiap kali Anda mengetahui kedalaman, sempitkan cakupan terlebih dahulu, dan filter hanya subset alih-alih menggunakan $.. menggantikan level yang diketahui. Letakkan kondisi sedini mungkin: $.store.book[?(@.price < 10)] memindai jauh lebih sedikit node tidak relevan daripada $..[?(@.price < 10)]. Untuk dokumen dengan lebih dari 100.000 node, lakukan ini terlebih dahulu.