FAQ su interrogazione e validazione: JSONPath e Schema
In sintesi: Se le query di percorso devono usare JSONPath o Pointer, come scegliere tra Patch e Merge Patch, cosa può bloccare Schema e come scegliere una versione draft — tutto spiegato in un colpo.

In sintesi: usa JSONPath per trovare nodi per condizione, JSON Pointer o JSON Patch per indirizzare e modificare posizioni esatte, e Merge Patch per unire configurazioni; fissa i nuovi Schema su 2020-12 e attiva allErrors così ogni errore torna in un unico passaggio.
L'interrogazione è "trovalo"; la validazione è "conferma che sia corretto". Ognuna ha il proprio standard, quindi non mescolarle.
1. Query di percorso
Come scelgo tra JSONPath e JSON Pointer?
Dipende da se stai "trovando per condizione" o "recuperando per posizione".
| Dimensione | JSONPath | JSON Pointer |
|---|---|---|
| Standard | RFC 9535 (2024) | RFC 6901 (2013) |
| Espressione | $.users[?(@.age > 18)].id |
/users/0/id |
| Wildcard e ricorsione | Supporta *, .. |
Non supportati |
| Filtro condizionale | Supportato | Non supportato |
| Risultato | Un insieme di nodi (possibilmente diversi) | Una singola posizione |
| Espressività | Forte, ma con un pesante retaggio di differenze tra implementazioni | Debole, ma pienamente deterministica |
| Uso tipico | Estrazione da log, debug, esplorazione dei dati | JSON Patch, indirizzamento di configurazione, percorsi di errore |
La regola di scelta è semplice:
- Devi "ottenere tutti i nodi che corrispondono a una condizione" → JSONPath;
- Devi "fissare una posizione esatta e aggiungerla, rimuoverla o modificarla" → JSON Pointer;
- Usali insieme: trova il nodo con JSONPath e descrivi la sua posizione con Pointer.
Non usare JSONPath per l'indirizzamento in un contratto API. JSONPath restituisce un insieme, e i tre rami per risultati vuoti, a un elemento e a più elementi rendono il protocollo complicato; Pointer significa "o punta a una posizione esistente o non esiste", il che si adatta meglio a un contratto.
Qual è la differenza tra $.. e $.*?
$.. è la discesa ricorsiva e percorre ogni livello; $.* restituisce solo i figli diretti del livello corrente.
{
"a": { "id": 1, "child": { "id": 2 } },
"b": { "id": 3 }
}
$.*→ i due oggetti{id:1, child:{...}}e{id:3};$..id→ tre valori:1,2,3;$.*.id→1e3, escludendo il2annidato.
La discesa ricorsiva è comoda ma costosa: $.. percorre l'intero albero, che su un documento grande è O(n), e raccoglie facilmente campi profondi che non intendevi prendere.
Se funziona un livello concreto, evita ...
I filtri possono confrontare stringhe? Possono corrispondere con regex?
Il confronto tra stringhe funziona; la regex dipende dall'implementazione.
Sintassi standard (RFC 9535):
$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)] // compare with another node
Il confronto tra stringhe usa virgolette doppie (le virgolette singole sono ammesse anche in RFC 9535, ma alcune implementazioni più vecchie accettano solo le doppie).
Il confronto numerico segue la semantica numerica; mescolare una stringa con un numero, come "10" > 9, è comportamento indefinito nello standard, quindi non farci affidamento.
Per le regex, RFC 9535 definisce le funzioni match() e search(), ma il loro supporto varia notevolmente tra le implementazioni.
L'approccio sicuro: lascia che JSONPath faccia il filtro grossolano, e fai la corrispondenza regex nel codice dopo aver recuperato i risultati.
Devo usare JSON Patch o Merge Patch?
Entrambi descrivono "come cambiare qualcosa", ma la loro espressività e semantica sono completamente diverse.
| Dimensione | JSON Patch (RFC 6902) | Merge Patch (RFC 7386) |
|---|---|---|
| Forma | Un array di operazioni [{"op":"add",...}] |
Un oggetto parziale |
| Operazioni supportate | add / remove / replace / move / copy / test | Solo merge (eliminazione tramite null) |
| Può indirizzare elementi di array | Sì, tramite indice | No, l'intero array viene sostituito |
| Atomicità | test consente il locking ottimistico |
Nessuna |
| Dimensione del documento | Grande ma preciso | Piccola ma limitata nell'espressività |
| Adatto a | Modifiche granulari, aggiornamenti parziali di API | Unione di configurazioni, invio parziale di form |
Criteri di decisione:
- Per cambiare un campo del terzo elemento di un array → JSON Patch;
- Per unire diverse voci di configurazione nella configurazione esistente → Merge Patch;
- Per ottenere il controllo di concorrenza "fallisci se il valore originale non è X" →
testdi JSON Patch.
Nota la trappola nella semantica di null di Merge Patch: null significa "elimina questa chiave", non "imposta il valore a null".
Impostare un valore a null è impossibile con Merge Patch.
2. Validazione con Schema
Quali problemi risolve JSON Schema?
Trasforma "come dovrebbero apparire i dati" da un accordo verbale in un contratto verificabile dalla macchina. Gli errori che può davvero bloccare:
- Campi obbligatori mancanti (
required); - Tipo errato, per esempio una stringa dove ci si aspetta un numero (
type); - Valori fuori intervallo (
minimum/maximum/minLength); - Valori fuori dall'insieme consentito (
enum); - Tipi di elementi dell'array incoerenti o lunghezza dell'array errata (
items/minItems); - Campi mancanti in strutture annidate (validazione ricorsiva tramite
properties); - Campi in eccesso (
additionalProperties: false).
Ciò che non può risolvere: regole di business ("l'ora di fine deve essere successiva all'ora di inizio"), condizioni tra campi (alcune possono essere espresse con if/then,
ma la leggibilità peggiora), unicità nel database e controlli di permessi.
Quindi Schema è il guardiano della "forma dei dati", non un sostituto della logica di business.
Devo usare draft-07 o 2020-12?
Usa 2020-12 per i nuovi progetti; per i sistemi esistenti, controlla cosa supporta il validatore.
| Versione | Stato | Modifiche chiave |
|---|---|---|
| draft-04 | Superata | Molte librerie più vecchie la usano ancora come default |
| draft-07 | Standard de facto | Supporto più ampio, ecosistema più ricco |
| 2019-09 | Transitoria | Ha introdotto $defs, unevaluatedProperties |
| 2020-12 | Attualmente raccomandata | items diviso in prefixItems + items, supporta $dynamicRef |
Due trappole di migrazione:
- La sintassi delle tuple è cambiata: in draft-07 un
itemscon valore array esprimeva una tupla; 2020-12 l'ha spostata inprefixItems; $refe parole chiave sorelle: in draft-07 le parole chiave sorelle vengono ignorate quando$refè presente; 2020-12 consente loro di coesistere.
Se il validatore supporta solo draft-07, scrivere comunque sintassi 2020-12 viene ignorato silenziosamente — e il caso più pericoloso è "la validazione passa" quando in realtà non è stato validato nulla.
Come genero i tipi TypeScript da uno Schema?
Due percorsi: derivare i tipi dallo Schema, o generare lo Schema dai tipi.
Da Schema a tipi (il backend definisce il contratto, il frontend lo consuma):
import type { FromSchema } from 'json-schema-to-ts';
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
age: { type: 'integer' },
},
required: ['id'],
additionalProperties: false,
} as const;
type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }
La chiave è as const: senza di essa, type: 'string' viene inferito come string invece che come il letterale 'string', e l'inferenza si rompe.
Da tipi a Schema (il frontend ha già i tipi e necessita di validazione a runtime):
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);
Il vantaggio di questo percorso è che "una definizione produce sia i tipi sia la validazione a runtime"; il costo è introdurre un DSL,
e lo Schema generato può portare dettagli implementativi (per esempio una struttura $ref poco attraente).
Come localizzo il campo specifico quando la validazione fallisce?
Guarda l'instancePath (2020-12) o il dataPath (draft-07) restituito dal validatore; è un JSON Pointer alla posizione dell'errore.
[
{
"instancePath": "/items/2/price",
"schemaPath": "#/properties/items/items/properties/price/type",
"keyword": "type",
"message": "must be number"
}
]
instancePath: "/items/2/price" ti dice direttamente: il campo price del terzo elemento dell'array items ha il tipo sbagliato.
Due suggerimenti pratici:
- Restituisci tutti gli errori in una volta invece di lanciare sul primo — il frontend che sistema tutto in un colpo è più veloce dei tentativi andata e ritorno;
- Trasforma il Pointer in linguaggio semplice prima di mostrarlo agli utenti finali;
/items/2/priceè utile agli sviluppatori e inutile agli utenti.
allErrors: true è l'interruttore nella maggior parte dei validatori (Ajv richiede di attivarlo esplicitamente); il default è spesso la modalità fail-fast.
additionalProperties: false dovrebbe essere attivato?
Attivalo per i contratti interni; sii prudente con le API pubbliche.
Una volta attivato, qualsiasi campo non dichiarato sotto properties fa fallire la validazione. Il vantaggio è intercettare errori di battitura
(userNmae) e passaggi inattesi; lo svantaggio è che l'aggiunta di un campo a monte rompe i consumatori a valle, distruggendo la retrocompatibilità.
Compromesso:
| Scenario | Raccomandazione |
|---|---|
| Tra servizi interni | Attivalo. Le modifiche ai campi passano attraverso la gestione delle versioni |
| API pubbliche esterne | Disattivalo. Consenti all'upstream di aggiungere campi |
| Consumo di dati di terze parti | Disattivalo. Non controlli l'altra parte |
| Validazione della configurazione | Attivalo. Un errore di battitura nella configurazione deve fallire subito |
Come valido che gli elementi di un array siano unici?
Due semantiche diverse — non confonderle:
{ "uniqueItems": true }
Questo deduplica l'intero elemento, richiedendo che l'array non contenga due elementi profondamente uguali.
{
"type": "array",
"items": { "type": "object", "properties": { "id": { "type": "string" } } },
"uniqueItems": true
}
Lo snippet sopra garantisce solo che gli oggetti nel loro insieme non siano duplicati; {"id":"a","name":"x"} e {"id":"a","name":"y"} non contano come duplicati.
Per deduplicare in base a un campo specifico, Schema non può esprimerlo; devi validare nel livello di business:
const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
throw new Error('id 重复');
}
format valida davvero email e date?
Non necessariamente. Nella specifica, format è un'"annotazione" per impostazione predefinita e non un'"asserzione", quindi un validatore può raccoglierlo senza validarlo.
- Ajv richiede di installare e configurare il pacchetto separato
ajv-formats, altrimentiformat: "email"è di fatto inerte; - Alcuni validatori trattano
formatcome un'asserzione attivata per impostazione predefinita, quindi il comportamento è incoerente; - Anche quando è attivata, la regex di
emailè molto permissiva —a@bpassa.
Conclusione: non trattare format come validazione forte. Fai una validazione rigorosa di email, URL e date nel codice di business;
mantenere format nello Schema come documentazione vale la pena, ma non aspettarti che fermi dati sporchi.
3. Riferimento rapido
| Domanda | Conclusione |
|---|---|
| Trovare nodi per condizione | JSONPath |
| Indirizzamento preciso, applicare patch | JSON Pointer |
| Cambiare elementi di un array | JSON Patch; Merge Patch non può farlo |
| Unire configurazioni | Merge Patch |
| Versione di Schema per nuovi progetti | 2020-12, attenzione a prefixItems |
| Generare tipi TS da uno Schema | json-schema-to-ts + as const |
| Localizzare gli errori di validazione | Leggi instancePath, attiva allErrors |
| Deduplicare per campo | Schema non può; usa un Set nel livello di business |
format: email |
La maggior parte dei validatori non lo controlla per impostazione predefinita, quindi non farci affidamento |