JSON Toolkit

Quando gli errori di JSON Schema non hanno senso: posizione degli errori Ajv e governance dei messaggi

In sintesi: Un validatore emette errori leggibili dalla macchina; gli utenti vogliono sapere quale casella di input è sbagliata. Copre l'array errors di Ajv, instancePath versus schemaPath, raggruppamento e formulazione.

Avanzato~10 min di letturaPubblicato il 2026-10-31Aggiornato il 2026-10-31
Diagramma della posizione degli errori di validazione Ajv e della governance dei messaggi

In sintesi: leggi prima instancePath per localizzare il dato che fallisce, poi schemaPath per localizzare la regola che è scattata; per required e additionalProperties, estrai il vero nome della chiave da params prima di scegliere il messaggio.

Il compito del validatore è dire quale regola è fallita. Tradurlo in "questa casella di input è sbagliata" è compito tuo.

1. Cosa ti dà davvero l'array errors

Ajv non lancia un errore sul fallimento della validazione; lascia il risultato in validate.errors. La configurazione predefinita mantiene solo il primo errore, e allErrors è ciò che produce un elenco completo. Un singolo oggetto errore ha cinque campi che contano, e capirli è la premessa per tutto ciò che segue:

Campo Significato Esempio
instancePath JSON Pointer al valore che fallisce /email
schemaPath Posizione della parola chiave che fallisce nello schema #/properties/email/format
keyword Nome del vincolo violato format
params Parametri leggibili dalla macchina di quel vincolo { "format": "email" }
message Prosa inglese che cambia tra versioni must match format "email"

Un vero errore appare così:

[
  {
    "instancePath": "/email",
    "schemaPath": "#/properties/email/format",
    "keyword": "format",
    "params": { "format": "email" },
    "message": "must match format \"email\""
  }
]

Nota che message è prosa scritta per gli umani, non un'interfaccia stabile. Ajv può riformularla tra versioni, e non può servire un prodotto localizzato. Non analizzarla mai con un'espressione regolare, e non restituirla mai alla lettera a un client.

2. instancePath e schemaPath rispondono a domande diverse

instancePath dice quale porzione dei dati è fallita; schemaPath dice quale regola è scattata. Sono due sistemi di coordinate, e non sono simmetrici:

  • instancePath è un JSON Pointer RFC 6901. Una stringa vuota significa la radice; /items/0/price significa il campo price del primo elemento dell'array.
  • schemaPath inizia con # e punta alla parola chiave dentro il documento dello schema. Attraverso un $ref, punta dentro lo schema referenziato, quindi necessiti dell'$id per sapere da quale file proviene.
  • Una chiave che contiene / o ~ viene sottoposta a escape rispettivamente in ~1 e ~0. Annulla quell'escape prima della visualizzazione, o l'interfaccia mostrerà simboli incomprensibili.

Per esempio, instancePath: "/email" insieme a schemaPath: "#/properties/email/format" significa che il campo email sull'oggetto radice non ha superato il controllo del formato.

La trappola più comune è required e additionalProperties: il loro instancePath punta all'oggetto padre, non alla chiave che crea il problema.

Parola chiave instancePath punta a Dove vive la vera chiave
required L'oggetto padre (stringa vuota alla radice) params.missingProperty
additionalProperties L'oggetto padre params.additionalProperty
type Il valore che fallisce stesso —
enum Il valore che fallisce stesso params.allowedValues

Quando mappi gli errori sui campi del form, gestisci prima questi due casi. Altrimenti "email manca" diventa "errore dell'oggetto radice", e l'utente non ha idea di dove digitare.

3. allErrors e verbose: completezza versus costo

Il default allErrors: false fa fermare Ajv al primo fallimento. Va bene per uno strumento da riga di comando ed è disastroso per un form: l'utente corregge un campo, invia, vede l'errore successivo, e ripete tre volte prima che il form passi.

Impostazione Comportamento Beneficio Costo
allErrors: false Solo il primo errore Il più veloce, memoria minima Invii ripetuti
allErrors: true Raccoglie ogni errore Tutti i problemi in un passaggio Il numero di errori esplode su schema complessi
verbose: true Allega schema e data a ogni errore Diagnosi lato server più facile Dimensione molto aumentata; non esporla mai
messages: false Elimina il campo message Payload più piccoli Perdi l'indizio inglese di debug

Divisione raccomandata: abilita allErrors: true per la validazione lato server e mantieni l'array completo nei log interni, mentre la risposta pubblica porta solo campi in whitelist. Attiva verbose solo durante l'indagine; incorpora il frammento di schema corrispondente e i dati dell'utente in ogni errore, il che costa memoria e può far trapelare valori sensibili nei log.

Il volume degli errori è l'altro problema comune. Un ramo anyOf fallito emette diversi errori figli più un errore di riepilogo, e oneOf con molti rami è peggio. Mostra l'array così com'è e gli utenti vedono cinque messaggi quasi duplicati per un campo. Il raggruppamento deve avvenire prima del rendering.

4. Raggruppare gli errori per campo

Il raggruppamento fa tre cose: trasforma il percorso in un nome di campo, corregge l'offset di required e additionalProperties, e unisce più errori sullo stesso campo nell'unico messaggio più azionabile.

/** One validation error ready for display */
interface FieldError {
  field: string;
  keyword: string;
  params: Record<string, unknown>;
}

/**
 * Group Ajv errors by field.
 * @param errors Error array produced by Ajv
 * @returns Map keyed by field path, each value listing its errors
 */
function groupByField(errors: Array<Record<string, any>>): Record<string, FieldError[]> {
  const grouped: Record<string, FieldError[]> = {};

  for (const errorItem of errors) {
    const keyword: string = errorItem.keyword;
    let field: string = errorItem.instancePath;

    if (keyword === 'required') {
      const parentPath: string = errorItem.instancePath;
      field = `${parentPath}/${errorItem.params.missingProperty}`;
    } else if (keyword === 'additionalProperties') {
      field = `${errorItem.instancePath}/${errorItem.params.additionalProperty}`;
    }

    const normalizedField = field === '' ? '(root)' : field;
    (grouped[normalizedField] ??= []).push({
      field: normalizedField,
      keyword,
      params: errorItem.params ?? {},
    });
  }

  return grouped;
}

Unisci con un ordine di priorità: required (mancante) per primo, poi type, poi enum/const, poi format/pattern, poi lunghezza e intervallo. Mostra solo il messaggio di priorità più alta per campo e tieni il resto nei log. La ragione è semplice: quando il tipo è sbagliato, un messaggio di lunghezza è rumore, e correggere il tipo è l'unico passo sensato successivo.

Gli errori di riepilogo di oneOf e anyOf di solito non portano informazioni azionabili. Saltali durante il rendering e mantieni solo gli errori dei rami che puntano a un campo concreto.

5. Messaggi personalizzati: la via del plugin e la via del template

Ajv non fornisce formulazione localizzata, quindi ci sono due opzioni pratiche.

Opzione uno: il plugin ajv-errors. Aggiunge una parola chiave errorMessage così i messaggi vivono accanto ai vincoli:

{
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "errorMessage": {
        "format": "Please enter a valid email address",
        "type": "Email must be a string"
      }
    }
  }
}

Il vantaggio è che la formulazione viaggia con il campo e lo schema funge anche da documentazione. Lo svantaggio è che il testo è congelato in una lingua, quindi la localizzazione significa mantenere diversi schema. Il plugin inoltre sostituisce ogni messaggio solo quando allErrors: true è impostato; dimentica quel flag e ottieni un mix di messaggi personalizzati e inglese predefinito.

Opzione due: una tabella parola-chiave/template. Il validatore fornisce solo keyword e params; l'applicazione produce la formulazione:

Parola chiave Template Parametri disponibili
required Campo obbligatorio mancante: {field} missingProperty
type Tipo atteso: {type} type
minLength Inserisci almeno {limit} caratteri limit
maximum Non deve superare {limit} limit
format Formato invalido: {format} format
enum Deve essere una delle opzioni consentite allowedValues
pattern Il valore non corrisponde al formato atteso pattern (non ripeterlo)

L'opzione due si adatta ai prodotti che necessitano più lingue e una voce coerente. Separa la validazione dalla presentazione, tiene la traduzione fuori dallo schema, e permette al frontend di riusare gli stessi template.

6. Localizzazione: i messaggi non appartengono al validatore

Spostare la formulazione nei language pack è il punto di partenza della governance dei messaggi. Un language pack memorizza template e chiavi, mai nomi di campi; le etichette dei campi vengono dalla configurazione del form o dal contratto API, e i due si combinano al momento del rendering.

/** Validation message templates keyed by Ajv keyword */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
  required: (params) => `Missing required field: ${params.missingProperty}`,
  type: (params) => `Expected type: ${params.type}`,
  minLength: (params) => `Enter at least ${params.limit} characters`,
  maximum: (params) => `Must not exceed ${params.limit}`,
  format: () => 'Invalid format',
};

/**
 * Build one user-facing validation message.
 * @param keyword Name of the failed constraint
 * @param params Constraint parameters
 * @returns Localized message text
 */
function buildMessage(keyword: string, params: Record<string, any>): string {
  const template = VALIDATION_TEMPLATES[keyword];
  return template ? template(params) : 'The value is invalid';
}

Tre regole contano qui:

  1. Non concatenare frasi. "Il formato email è invalido" e il suo equivalente italiano non condividono l'ordine delle parole, e l'assemblamento di stringhe finirà per produrre nonsenso in una delle due.
  2. Delega plurali e classificatori alla libreria i18n. Non codificare mai "caratteri" in un template; 1 carattere e 3 caratteri differiscono.
  3. Traduci anche le etichette dei campi. params.missingProperty è email, mentre l'interfaccia dovrebbe dire "Indirizzo email". Mantieni quella mappatura nella configurazione del form, non nel language pack.

7. Non restituire la struttura interna ai client

Restituire insieme schemaPath, params e message è comodo durante lo sviluppo e una fuga di informazioni in produzione. allowedValues rivela enum interni, pattern rivela regex di validazione, e schemaPath rivela layout dei file e denominazione dei campi — in alcuni casi consegnando agli attaccanti una mappa dei tuoi punti deboli.

La risposta pubblica mantiene esattamente tre cose: un percorso di campo (o nome di campo del form), un codice di errore stabile, e testo rivolto all'utente. Usa codici brevi e stabili invece del keyword grezzo così mantieni spazio per cambiare validatore in seguito.

{
  "type": "about:blank",
  "title": "Request validation failed",
  "status": 422,
  "errors": [
    { "field": "email", "code": "invalid_format", "message": "Please enter a valid email address" },
    { "field": "items[0].price", "code": "invalid_pattern", "message": "Amount must have exactly two decimals" }
  ]
}

I log del server dovrebbero contenere l'opposto: l'array completo degli errori, l'id della richiesta e la versione dello schema, così un'indagine può rispondere "quale regola ha bloccato questo". Evita di versare interi corpi di richiesta nei log; mantieni invece i percorsi e le parole chiave che falliscono.

8. Presentazione frontend: dall'evidenziazione del campo ai marker dell'editor

Gli errori finiscono eventualmente in un'interfaccia, e la presentazione dipende dal contesto:

Contesto Presentazione Azione chiave
Form Testo rosso sotto il campo, focus sul primo errore Mappa i percorsi ai nomi del form; mantieni gli indici di array
Import massivo di tabelle Numero di riga, nome di colonna, riepilogo, esportabile Trasforma /items/3/price in riga 4, colonna price
Editor JSON Marker inline e pannello dei problemi Usa un parser per mappare JSON Pointer a riga e colonna
Strumento di debug API Elenco strutturato di errori che conserva schemaPath Rivolto agli sviluppatori, quindi più dettaglio è accettabile

Tre note implementative:

  • Mostra solo il primo errore per campo, raggruppando il resto in "altri 2 problemi", così il form non annega nel rosso.
  • Mantieni gli indici di array per il targeting ma eliminali nelle etichette. "Riga 4, prezzo" si legge meglio di /items/3/price.
  • Gli scenari di editor necessitano mappatura delle posizioni. Una semplice ricerca di stringa non è affidabile quando un nome di campo si ripete; il parser dovrebbe fornire gli offset per ogni nodo, che poi converti in riga e colonna.

Se il JSON proviene da un utente che digita invece che da un form, conferma prima che il documento sia sintatticamente valido. Gli errori di sintassi avvengono durante il parsing e non raggiungono mai la validazione dello schema. Parsing e formattazione JSON riporta la riga e la colonna esatte, il che è molto più veloce che inseguire gli errori di schema. Per come nascono gli schema troppo rigidi quando vengono inferiti da campioni, vedi Perché lo JSON Schema generato automaticamente continua a rifiutare i dati reali; per la configurazione del validatore e il sistema delle parole chiave, vedi JSON Schema: dalle basi alla produzione.

9. Domande frequenti

Perché ottengo solo un errore?

allErrors è false per impostazione predefinita, quindi Ajv restituisce al primo fallimento. Attivalo esplicitamente per la validazione dei form; altrimenti gli utenti devono inviare ripetutamente per scoprire ogni problema.

Perché il percorso su un errore required è vuoto?

instancePath per required punta all'oggetto padre, che è la stringa vuota alla radice; il nome della chiave mancante sta in params.missingProperty. Concatena i due per ottenere il percorso completo. Mostrare instancePath da solo produce l'inutile "errore dell'oggetto radice".

Posso mostrare message direttamente agli utenti?

Non è consigliabile. È prosa inglese la cui formulazione cambia tra versioni, e non ha capacità di localizzazione. Genera il testo da keyword e params e riserva message ai log.

Perché il mio messaggio di errore personalizzato viene ignorato?

Controlla prima che allErrors: true sia abilitato; ajv-errors può sostituire tutti i messaggi solo con quel flag. Poi conferma che errorMessage stia allo stesso livello della parola chiave che fallisce — un livello sbagliato e non verrà riconosciuto.

Quali campi sono sicuri da restituire in produzione?

Percorso di campo, codice di errore stabile e testo rivolto all'utente. Mantieni schemaPath, params e message nei log del server: supporta il debug senza esporre enum interni, regex o layout dei file.