Il linguaggio di interrogazione JSONPath in pratica
In sintesi: Nel 2024 JSONPath ha finalmente ottenuto un vero standard formale, RFC 9535. Questo articolo spiega la sintassi, la semantica dei filtri, le funzioni e le differenze storiche tra implementazioni.

In sintesi: localizza prima con un percorso esplicito e filtra sopra di esso, e scrivi le espressioni secondo RFC 9535; usa l'accesso alle proprietà o JSON Pointer per un singolo campo fisso, e jq o il codice per l'aggregazione.
Hai probabilmente visto espressioni come
$.store.book[?(@.price < 10)].title, ma è diventato uno standard internazionale solo nel 2024.
1. Da standard de facto a standard formale
JSONPath è stato proposto da Stefan Gössner nel 2007, e per molto tempo l'unica "specifica" era un singolo post di blog, con ogni implementazione di linguaggio che faceva di testa propria:
- se i filtri necessitano parentesi (
?(@.a)vs?@.a); - se l'indice di array
[-1]è supportato; - se il risultato restituisce "valori" o "valori e percorsi";
- se le script expression (
?(@.price < $.max)) sono ammesse.
RFC 9535, pubblicato a febbraio 2024, ha convergito queste divergenze in un'unica sintassi e semantica e ha fornito una suite ufficiale di test di conformità. I nuovi progetti dovrebbero seguire RFC 9535; quando si migrano progetti vecchi, conferma prima la versione dell'implementazione.
2. Cheat sheet della sintassi
| Sintassi | Significato | Esempio |
|---|---|---|
$ |
Nodo radice | $.store |
@ |
Nodo corrente dentro un filtro | $[?@.price < 10] |
.name |
Nodo figlio (il nome deve corrispondere a [A-Za-z_][A-Za-z0-9_]*) |
$.store.book |
['name'] |
Nodo figlio (qualsiasi nome, inclusi non ASCII e punti) | $['store']['书'] |
[0] |
Indice di array; i numeri negativi contano dalla fine | $.book[0], $.book[-1] |
[0,2] |
Indici multipli / nomi multipli | $.book[0,2] |
[1:3] |
Slice, inizio incluso e fine esclusa | $.book[1:3] |
[::2] |
Slice con uno step | $.book[::2] |
[*] |
Wildcard per tutti i nodi figli | $.book[*].title |
..name |
Discesa ricorsiva, cercando a qualsiasi profondità | $..price |
..[*] |
Prende ricorsivamente tutti i nodi | $..* |
[?<expr>] |
Selettore di filtro | $.book[?@.price < 10] |
Una semantica che devi ricordare: JSONPath restituisce un insieme di nodi (nodelist), non un singolo valore. Anche quando corrisponde un solo nodo, il risultato è una lista di lunghezza uno.
3. Percorrere la sintassi con un esempio
{
"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
}
| Espressione | Risultato |
|---|---|
$.store.bicycle.color |
["red"] |
$..price |
4 prezzi (incluso il prezzo della bicicletta) |
$.store.book[*].title |
3 titoli |
$.store.book[?@.price < 10].title |
Titoli dei libri 1 e 3 |
$.store.book[?@.tags] |
Tutti i libri che hanno tags (test di esistenza) |
$.store.book[-1].title |
Titolo dell'ultimo libro |
$..book[0,1] |
I primi due libri |
$.store.book[?@.price < $.expensive] |
Sintatticamente dipendente dall'implementazione; RFC 9535 non permette i riferimenti alla radice |
L'ultima riga è una trappola storica: molte implementazioni consentono $.expensive dentro un filtro per il confronto tra nodi, cosa che RFC 9535 vieta esplicitamente —
un filtro può accedere solo al nodo corrente @. Quando devi confrontare con una soglia globale, passa la soglia come argomento alla funzione di query.
4. Cosa possono esprimere i filtri
Le espressioni di filtro RFC 9535 supportano quattro tipi di costrutti:
Logic: && || ! e.g. ?@.price < 10 && @.tags
Comparison: == != < <= > >=
Existence: a bare path e.g. ?@.tags (tests existence)
Functions: length() count() match() search() value()
Alcuni esempi pratici:
// 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() è una corrispondenza sull'intera stringa e search() è una ricerca di sottostringa; il dialetto regex sottostante è I-Regexp (un sottoinsieme di RFC 9485),
che non supporta né backreference né lookaround — il che è sia un limite sia uno scudo utile contro il regex DoS.
5. Differenze tra implementazioni: controlla prima di migrare
| Implementazione | Linguaggio | Supporto RFC 9535 | Note |
|---|---|---|---|
jsonpath-plus |
JavaScript | Per lo più compatibile | Supporta funzioni estensive e script expression; più capace ma non rigorosamente standard |
jsonpath (dchester) |
JavaScript | Parziale | Libreria di lunga data con sintassi ?() e una struttura di risultato diversa |
ojg / gjson |
Go | ojg ha un buon supporto |
gjson usa un'altra sintassi di percorso e non è JSONPath |
jsonpath-ng |
Python | Parziale | Supporta estensioni ed è abbastanza permissiva |
Jayway JsonPath |
Java | Parziale | Ecosistema ampio; il comportamento storico differisce dall'RFC |
Tre controlli prima di migrare:
- Se le parentesi dei filtri sono richieste;
- Se indizi negativi come
-1sono supportati; - Se il risultato è un "array di valori" o un "array di oggetti
{path, value}".
Il terzo è il più facile in cui inciampare: il frontend legge result[0], e dopo aver cambiato libreria riceve un oggetto, lasciando la pagina bianca.
6. Prestazioni e sicurezza
Prestazioni
- JSONPath è essenzialmente un singolo attraversamento con complessità O(numero di nodi), e non può usare indici;
- La discesa ricorsiva con
..è la più costosa; se puoi localizzare prima con$.store.booke poi filtrare, non scrivere$..book; - Compila le espressioni una volta e riusale (la maggior parte delle librerie offre
compileo caching) invece di ri-analizzare a ogni query; - Per documenti con più di 100.000 nodi, considera prima l'estrazione esatta con JSON Pointer, o la pre-elaborazione sul server con
jq.
Sicurezza
- Le espressioni provenienti dall'input utente devono avere limiti di lunghezza e profondità di annidamento;
- Non lasciare che JSONPath gestisca l'autorizzazione — può solo leggere, e non può esprimere "quali campi sono invisibili";
- Quando la regex usata da
match()viene dagli utenti, limitane la complessità; sebbene sia più sicura di PCRE, un limite di lunghezza resta raccomandato.
7. Quando non usare JSONPath
- Ti serve solo un campo fisso → usa direttamente
data.store.bicycle.color; - Devi riscrivere dopo aver modificato → usa JSON Pointer o una patch; JSONPath non riscrive;
- Ti serve aggregazione (somma, raggruppamento) → usa
jq, SQL o codice; JSONPath seleziona soltanto; - Il documento è enorme e ti servono solo pochi campi → usa un parser in streaming invece di leggere prima l'intero albero in memoria.
8. Lista di controllo
- [ ] Le espressioni seguono RFC 9535 e non dipendono da riferimenti alla radice;
- [ ] È chiaro che il risultato è un insieme di nodi; usa
[0]ovalue()per ottenere un singolo valore; - [ ] Le espressioni sono precompilate e riusate;
- [ ] Le espressioni controllate dall'utente hanno limiti di lunghezza e complessità;
- [ ] I test case di conformità ufficiali vengono eseguiti quando si cambia implementazione.
FAQ
Come scelgo tra JSON Pointer e JSONPath?
Usa l'accesso semplice alle proprietà o un JSON Pointer quando ti serve un solo campo fisso; ricorri a JSONPath quando ti servono filtri, wildcard o discesa ricorsiva. JSONPath seleziona soltanto e non riscrive mai, quindi la scrittura appartiene a JSON Pointer o a una patch.
Perché un filtro non può referenziare $.expensive?
RFC 9535 vieta esplicitamente i riferimenti alla radice; un filtro può accedere solo al nodo corrente @. Passa invece la soglia come argomento alla funzione di query. Le implementazioni più vecchie accettano la sintassi, ma si rompe quando cambi o aggiorni libreria.
Devo affidarmi a result[0] per leggere un valore?
Solo dopo aver confermato che l'implementazione corrente restituisce un array di valori. Alcune librerie restituiscono oggetti {path, value}, quindi result[0] diventa un oggetto e la pagina diventa bianca. Leggi i singoli valori attraverso [0] o value(), ed esegui la suite di conformità ufficiale prima di cambiare implementazione.