JSON Toolkit

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.

Intermedio~12 min di letturaPubblicato il 2026-10-30Aggiornato il 2026-10-30
Copertina di Il linguaggio di interrogazione JSONPath in pratica

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:

  1. Se le parentesi dei filtri sono richieste;
  2. Se indizi negativi come -1 sono supportati;
  3. 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.book e poi filtrare, non scrivere $..book;
  • Compila le espressioni una volta e riusale (la maggior parte delle librerie offre compile o 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] o value() 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.