Filtri JSONPath a fondo: ricorsione, slice e corrispondenza multi-condizione
In sintesi: La discesa ricorsiva, gli slice e i filtri multi-condizione sono dove JSONPath diventa potente e usato male. Copre la semantica dei filtri, le forme dei risultati e le differenze tra librerie.

In sintesi: localizza prima una profondità nota, poi aggiungi slice e filtri invece di ricorrere a $..; un percorso nudo in un filtro testa solo l'esistenza, quindi per testare un booleano usa @.stock == true.
Chiunque può scrivere
$.store.book[0].title. Ciò che davvero rompe JSONPath è combinare$..con slice e condizioni multiple, e ottenere troppi risultati, troppo pochi risultati, o un ordine che non ti aspettavi.
1. Un documento di esempio
{
"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"
}
Ogni espressione sotto usa questo documento come input. Tieni a mente due premesse: il risultato è un insieme di nodi, e il suo ordine segue l'ordine del documento invece dell'ordine in cui le condizioni sono state scritte. Dentro un filtro, @ si riferisce solo al nodo candidato corrente.
2. Discesa ricorsiva $..: comoda ma costosa
.. significa "cerca ricorsivamente a qualsiasi profondità dalla posizione corrente". RFC 9535 la definisce come segmento discendente, e può essere combinato con nomi, wildcard, indici, slice e filtri:
$..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
Tre cose da capire:
$..booktrova ogni nodo chiamatobook, indipendentemente dalla sua profondità;- L'attraversamento è depth-first, e l'ordine di output dipende da come l'implementazione visita i figli;
..non può usare alcun indice, quindi il suo costo scala con il numero totale di nodi.
Di conseguenza, $..price e $.store.book[*].price possono differire solo per il prezzo della bicicletta nel loro output, eppure differire di un ordine di grandezza nel costo. Quando conosci la profondità, scrivi sempre il percorso esplicito. Se vuoi solo confrontare i confini dei due linguaggi di percorso, vedi JSONPath vs JSON Pointer.
3. Slice e wildcard degli array: tre trappole
La sintassi dello slice è [start:end:step], con inizio incluso e fine esclusa, e numeri negativi che contano dalla coda:
| Espressione | Significato | Risultato (con quattro libri) |
|---|---|---|
[0:2] |
Primi due libri | Libri 1 e 2 |
[1:] |
Dal secondo libro alla fine | Libri 2, 3 e 4 |
[:2] |
Primi due libri | Libri 1 e 2 |
[-2:] |
Ultimi due libri | Libri 3 e 4 |
[::2] |
Un libro sì e uno no | Libri 1 e 3 |
[::-1] |
Ordine inverso | Libri 4, 3, 2 e 1 |
Tre trappole:
- Uno step di zero è invalido. RFC 9535 richiede uno step non nullo, mentre alcune implementazioni restituiscono silenziosamente un insieme vuoto;
- Gli step negativi non sono universalmente supportati. L'implementazione originale di Goessner li supporta, ma alcune implementazioni minimali accettano solo step positivi;
- Filtrare dopo uno slice non allarga lo scope.
[0:2][?(@.stock)]filtra solo i due libri prodotti dallo slice; non torna all'intero array.
La wildcard [*] può essere combinata con gli slice, ma [*] produce "tutti i nodi figli". Se quei figli sono oggetti, aggiungere .title dà tutti i titoli. Se un figlio è esso stesso un array, .title non appiattirà implicitamente un altro livello. Scrivi di nuovo [*] quando devi espandere; non aspettarti un appiattimento implicito.
4. Filtri multi-condizione: confronti, logica ed esistenza
Un filtro [?(...)] può contenere quattro tipi di costrutti: confronti, operatori logici, test di esistenza e funzioni. Una query multi-condizione comune appare così:
$..book[?(@.price < 10 && @.stock)]
Significa "prezzo sotto 10 e stock è truthy". Presta attenzione a diversi dettagli semantici:
@.stockè un test di esistenza, non "il valore è vero". Se il campo esiste ma il suo valore èfalse, la condizione vale comunque. Per testare un valore booleano devi scrivere@.stock == true;&&si lega più strettamente di||. Aggiungi parentesi quando li mescoli invece di affidarti alla memoria;- Entrambi i lati di un confronto devono avere lo stesso tipo. Numeri e stringhe non vengono convertiti implicitamente;
@.price < "10"è falso o un errore nelle implementazioni rigorose; - Un campo mancante rende sempre un confronto falso.
@.missing < 10non solleva un errore; semplicemente non corrisponde a nulla.
È il punto più facile in cui inciampare scrivendo filtri come se fossero JavaScript: if (obj.stock) testa la verità in JavaScript, mentre JSONPath testa l'esistenza. Per la sintassi standard completa e le funzioni, rivedi Il linguaggio di interrogazione JSONPath in pratica.
5. Script expression: più potere, meno portabilità
L'implementazione originale di Goessner e jsonpath-plus consentono confronti tra nodi come [?(@.price < $.max)], e persino script arbitrari. RFC 9535 permette esplicitamente solo l'accesso al nodo corrente @ e vieta $ dentro un filtro.
// 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 })
Le script expression creano tre problemi:
- La portabilità scende a zero. Cambiare libreria significa riscrivere ogni espressione;
- Il confine di sicurezza sparisce. Quando le espressioni vengono dagli utenti, la valutazione di script consegna l'esecuzione di codice;
- L'ottimizzazione statica diventa impossibile. L'implementazione può solo interpretare nodo per nodo, il che è lento e difficile da mettere in cache.
Se hai davvero bisogno di confronto tra nodi, passa la soglia come parametro di query e filtra una seconda volta nel codice dell'applicazione. È di solito più stabile che dipendere da una sintassi estensiva.
6. Forme dei risultati: pensi di ottenere valori, ma ottieni nodi
La stessa espressione può restituire almeno tre forme tra le librerie:
["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
Questo influisce direttamente su tre cose: come il frontend legge i risultati, come i risultati vengono deduplicati, e come i risultati della query diventano operazioni di scrittura. Il nodelist definito da RFC 9535 è un insieme di riferimenti a nodi, ma le implementazioni JavaScript di solito restituiscono solo valori perché i riferimenti non possono attraversare i confini di processo.
Consiglio pratico:
- Leggi sempre un singolo valore attraverso
[0]ovalue(); non assumere mai che il risultato sia uno scalare; - Quando devi riscrivere, scegli un'implementazione che restituisce JSON Pointer o costruisci tu stesso il pointer;
- Un risultato vuoto e un risultato
[null]sono diversi; controlla la lunghezza invece della verità.
7. Differenze tra librerie: esegui la suite di conformità prima di migrare
| Capacità | Goessner originale | jsonpath-plus | RFC 9535 rigoroso |
|---|---|---|---|
Parentesi dei filtri ?() |
Richieste | Compatibile | Opzionali |
Riferimento alla radice $.x |
Supportato | Supportato | Vietato |
| Script expression | Supportate | Supportate | Vietate |
| Indici e step negativi | Supportati | Supportati | Supportati |
| Restituire i percorsi | Configurabile | Configurabile | Non specificato |
Funzioni come length() |
Nessuna | Estensioni | Funzioni standard |
Prima di migrare, verifica almeno quattro cose: se le parentesi sono richieste, se $ può comparire dentro un filtro, come si comportano gli step negativi, e la struttura del risultato. La suite di test di conformità ufficiale copre la maggior parte delle differenze sintattiche e previene fallimenti silenziosi come "tre righe sono sparite dopo aver cambiato libreria".
8. Prestazioni e lista di controllo dell'uso improprio
Le prestazioni si riducono a una frase: JSONPath è un'attraversamento completo, e .. è il tipo più costoso di attraversamento completo.
- Per documenti con più di 100.000 nodi, restringi prima lo scope con un percorso esplicito, poi filtra il sottoinsieme;
- Compila un'espressione una volta e riusala; non ri-analizzare la stringa dentro un ciclo;
- Passa a
jqo al codice per l'aggregazione (somma, raggruppamento, ordinamento); JSONPath seleziona soltanto; - Metti le condizioni il prima possibile:
$.store.book[?(@.price < 10)]scansiona molti meno nodi irrilevanti di$..[?(@.price < 10)].
Lista di controllo dell'uso improprio:
- [ ] Trattare un test di esistenza come un test di verità booleana;
- [ ] Usare
$..invece di una profondità nota; - [ ] Dipendere da script expression per il confronto tra nodi;
- [ ] Assumere che il risultato sia sempre un array di valori;
- [ ] Consentire espressioni controllate dall'utente senza limiti di lunghezza e annidamento.
FAQ
Perché @.stock corrisponde ancora quando il campo è false?
Perché un percorso nudo dentro un filtro significa "il campo esiste", non "il valore è vero". Per testare il booleano, confronta esplicitamente con @.stock == true. È la più grande differenza semantica tra JSONPath e la verità di JavaScript.
L'ordine della discesa ricorsiva è stabile?
No. RFC 9535 specifica la semantica dell'insieme di nodi, e l'ordine di attraversamento dipende da come l'implementazione visita i figli. Quando ti serve un ordine fisso, localizza l'array e leggi per indice, oppure ordina i risultati nel codice dell'applicazione.
Slice e filtri possono essere combinati?
Sì, ma ricorda lo scope. $.store.book[0:2][?(@.stock)] filtra solo i due libri prodotti dallo slice. Per filtrare prima e fare lo slice dopo, scrivi $.store.book[?(@.stock)][0:2]; un ordine diverso dà un risultato diverso.
Perché jsonpath-plus supporta $.expensive quando lo standard lo vieta?
Perché i filtri sono definiti solo in termini del nodo corrente @. Un riferimento alla radice rende un'espressione dipendente dal contesto dell'intero documento, il che è difficile da analizzare staticamente e da portare tra linguaggi. Passa la soglia come parametro invece di incorporarla nell'espressione.
Come impedisco alla discesa ricorsiva di scansionare nodi irrilevanti?
Scrivi il percorso esplicito ogni volta che conosci la profondità, restringi prima lo scope, e filtra solo il sottoinsieme invece di usare $.. al posto di un livello noto. Metti le condizioni il prima possibile: $.store.book[?(@.price < 10)] scansiona molti meno nodi irrilevanti di $..[?(@.price < 10)]. Per documenti con più di 100.000 nodi, fallo per primo.