JSON Toolkit

JSON Schema: dalle basi alla produzione

In sintesi: Le parole chiave sono molte, ma solo una dozzina vengono usate ogni giorno. Metti a posto type/properties/required/$ref, gestisci bene i messaggi di errore, e copri l'ottanta per cento dei casi reali.

Intermedio~14 min di letturaPubblicato il 2026-10-30Aggiornato il 2026-10-30
Copertina di JSON Schema: dalle basi alla produzione

In sintesi: fissa prima $schema alla 2020-12, poi estrai le strutture condivise con $defs + $ref; usa oneOf solo quando deve corrispondere esattamente un ramo, e preferisci i discriminatori o anyOf ovunque altrove.

Uno schema non è un'appendice della documentazione; è un asset che può essere eseguito, testato e generato.

1. Chiarire prima la versione

Versione Valore di $schema Stato Note
draft-04 http://json-schema.org/draft-04/schema# Superata Progetti legacy e alcune toolchain OpenAPI 2.0
draft-07 http://json-schema.org/draft-07/schema# Ampiamente usata La base de facto di OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema Disponibile Ha introdotto $defs e il vocabolario delle annotazioni
2020-12 https://json-schema.org/draft/2020-12/schema Attualmente raccomandata Ha introdotto prefixItems, cambiato la semantica di items

Usa la 2020-12 per i nuovi progetti. Nota che nella 2020-12 items vincola solo gli "elementi rimanenti" — le dichiarazioni in stile tupla sono migrate a prefixItems. È la trappola più comune quando si migra da draft-07.

2. Uno schema minimo utilizzabile

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/user.json",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 64 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
  },
  "additionalProperties": false
}

Questo schema copre le quattro preoccupazioni più comuni: vincoli di tipo, campi obbligatori, limiti di intervallo e vincoli sugli elementi degli array.

3. Mappa delle parole chiave

3.1 Generali

Parola chiave Scopo Attenzione
type Il tipo; può essere un array "type": ["string", "null"] significa nullable
enum Valori enumerati I valori devono essere univoci
const Un vincolo a valore singolo Adatto a valori fissi come un numero di versione
$ref Referenzia un altro schema Referenzia l'intero schema e non si unisce alle parole chiave sorelle sull'oggetto che referenzia

3.2 Numeri

multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum. Nota che in draft-04 exclusiveMaximum era un booleano (usato insieme a maximum); solo dalla draft-07 è diventato una parola chiave numerica autonoma — il che fa fallire molti "schema copiati da documenti vecchi" sotto modalità strict.

3.3 Stringhe

minLength, maxLength, pattern (un'espressione regolare ECMA-262), e format (un'annotazione).

minLength conta code point Unicode, non unità UTF-16 e non byte. Validare con string.length di JavaScript sarà in disaccordo.

3.4 Array

Parola chiave Scopo
prefixItems Il vincolo in stile tupla della 2020-12, che abbina gli elementi uno per uno per indice
items Un vincolo uniforme sugli elementi rimanenti (2020-12)
contains + minContains/maxContains Almeno/al massimo quanti elementi corrispondenti devono essere presenti
uniqueItems Gli elementi devono essere tutti diversi (confronto profondo; evitalo su array grandi)
minItems / maxItems Vincoli di lunghezza

3.5 Oggetti

properties, required, patternProperties, additionalProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas.

Tre equivoci ad alta frequenza:

  1. required richiede solo che la chiave esista: {"name": ""} è valido. Per rifiutare una stringa vuota devi aggiungere minLength: 1;
  2. additionalProperties: false amplifica i problemi di compatibilità: un campo extra da un client fa fallire l'intero payload, quindi evolvere l'API forza ogni chiamante ad aggiornarsi in sincrono;
  3. properties non limita le chiavi non dichiarate — per impostazione predefinita sono ammesse.

3.6 Composizione logica

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
Parola chiave Semantica Fallimento tipico
allOf Tutti devono essere soddisfatti Più rami impongono vincoli in conflitto sullo stesso campo
anyOf Almeno uno deve essere soddisfatto Il messaggio di errore è difficile da ricondurre a un ramo specifico
oneOf Esattamente uno deve essere soddisfatto I dati corrispondono a due rami e falliscono, anche se "sembrano ovviamente corretti"
not Non deve essere soddisfatto Raramente usato, e i messaggi di errore meno amichevoli

oneOf è la parola chiave più propensa a produrre "dati validi che falliscono la validazione". Se i vincoli dei rami si sovrappongono (per esempio, entrambi ammettono campi extra), un singolo record corrisponde a diversi rami e oneOf fallisce del tutto. In quel caso, passa a anyOf, o usa un campo discriminatore con const dentro i rami.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

Lo stile con discriminatore è più leggibile di oneOf, e produce errori migliori.

4. Divisione e riutilizzo

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "address": {
      "type": "object",
      "required": ["city"],
      "properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
    }
  },
  "type": "object",
  "required": ["total", "shipTo"],
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "shipTo": { "$ref": "#/$defs/address" }
  }
}
  • $defs è la forma introdotta nella 2019-09; la draft-07 usa definitions;
  • I riferimenti tra file usano $id relativi, ma non affidarti al recupero di schema remoti a runtime in produzione — raggruppali come file statici;
  • Non puoi aggiungere vincoli accanto a un riferimento (il comportamento di $ref con parole chiave sorelle varia per versione). Quando devi comporre, usa allOf.

5. Validazione a runtime con Ajv

import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({
  allErrors: true,
  strict: true,
  allowUnionTypes: true,
});
addFormats(ajv);

const validateOrder = ajv.compile(orderSchema);

export function assertOrder(input: unknown): void {
  if (validateOrder(input)) {
    return;
  }
  const detail = (validateOrder.errors ?? []).map((errorItem) => ({
    path: errorItem.instancePath || '/',
    keyword: errorItem.keyword,
    message: errorItem.message,
    params: errorItem.params,
  }));
  throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}

Punti chiave:

  • Chiama compile una sola volta, al livello superiore del modulo; ricompilare a ogni richiesta trasforma una validazione da 10 ms in centinaia di millisecondi;
  • strict: true fa emergere subito le parole chiave scritte male (per esempio minlength invece di minLength), quindi vale la pena abilitarlo;
  • Solo ajv-formats fa sì che format: "email" e format: "date-time" abbiano davvero effetto;
  • La 2020-12 richiede il punto di ingresso ajv/dist/2020 — non usare l'export predefinito.

6. Come progettare i messaggi di errore

Ciò che il validatore ti dà è "quale parola chiave è fallita"; ciò che l'utente vuole è "quale casella di input è sbagliata". Vale la pena aggiungere uno strato di mappatura:

const FIELD_LABEL: Record<string, string> = {
  '/name': 'Name',
  '/age': 'Age',
  '/tags': 'Tags',
};

/**
 * Convert Ajv errors into display-ready messages.
 * @param errors The list of Ajv errors
 * @returns An array of display messages
 */
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
  return errors.map((errorItem) => {
    const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
    if (errorItem.keyword === 'required') {
      const missing = (errorItem.params as { missingProperty: string }).missingProperty;
      return `${label} is missing the required field ${missing}`;
    }
    if (errorItem.keyword === 'minLength') {
      return `${label} is too short`;
    }
    return `${label} failed validation (${errorItem.keyword})`;
  });
}

Lato API, mantieni una struttura leggibile dalla macchina così il frontend può decidere come presentarla:

{
  "type": "https://bbdrug.com/problems/validation",
  "title": "Request data validation failed",
  "status": 422,
  "errors": [
    { "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
  ]
}

7. Prestazioni e trappole

  • Il costo della validazione cresce con la complessità dello schema e la dimensione dei dati; più rami oneOf causano esplosione combinatoria, che diventa molto evidente con migliaia di record;
  • uniqueItems esegue un confronto profondo O(n²) sull'array; per array grandi, deduplica invece nel livello di business;
  • Evita i pattern di backtracking catastrofico in pattern (come (a+)+$); è il classico punto di ingresso per ReDoS;
  • Quando additionalProperties: false è combinato con allOf, i campi dichiarati in un sotto-schema vengono giudicati "extra" dal genitore, quindi devi unirli in un unico schema o usare unevaluatedProperties.

8. Lista di controllo

  • [ ] Fissa la 2020-12 e dichiara $schema all'inizio del file;
  • [ ] Estrai le strutture condivise con $defs + $ref invece di copia-incollare;
  • [ ] Preferisci i discriminatori a oneOf;
  • [ ] Riutilizza il risultato di compile del validatore e abilita la modalità strict;
  • [ ] Documenta chiaramente se format è imposto;
  • [ ] Le risposte di errore forniscono il percorso, la parola chiave e un messaggio rivolto all'utente;
  • [ ] Metti lo schema in CI: esegui i casi della JSON Schema Test Suite come test di regressione.

FAQ

Come dovrei scegliere un draft di JSON Schema?

Usa la 2020-12 per i nuovi progetti e dichiara $schema all'inizio del file. Se lo schema va in OpenAPI 3.1, deve anch'esso essere 2020-12, perché la 3.1 incorpora direttamente quella versione; mantieni draft-07 solo per i progetti ancora legati a toolchain più vecchie. I due si comportano diversamente per items e per le parole chiave sorelle di $ref, quindi non mescolarli in un unico insieme di schema.

Perché additionalProperties: false amplifica i problemi di compatibilità?

Tratta ogni chiave non dichiarata come un errore, quindi un campo extra da un client fa fallire l'intero payload. Ogni chiamante deve allora aggiornarsi in sincrono ogni volta che l'API evolve, o i client vecchi vengono rifiutati. Gli schema in stile configurazione dovrebbero chiudere esplicitamente i campi extra, mentre le API pubbliche devono valutare il costo di compatibilità.

Devo usare oneOf o anyOf?

Usa oneOf solo quando i dati devono corrispondere esattamente a un ramo. Se i vincoli dei rami possono valere contemporaneamente, un record corrisponde a diversi rami e oneOf fallisce del tutto; passa a anyOf, o aggiungi un discriminatore const dentro ogni ramo.

Riferimenti

Articoli correlati

Punti chiave per progettare contratti di dati API JSON

Sei cose sono molto difficili da cambiare una volta rilasciate: envelope delle risposte, formati degli errori, forma della paginazione, rappresentazione di ID e tempo, semantica di null versus assenza, ed evoluzione delle versioni.

Intermedio~13 min di letturaAggiornato il 2026-10-30

Senza additionalProperties, la validazione dello schema non valida nulla

Una validazione superata non significa dati puliti. Pydantic ignora i campi sconosciuti per impostazione predefinita, Zod li rimuove, Ajv li lascia passare — parametri allucinati e chiavi sbagliate attraversano in silenzio. L'articolo mostra le tre forme di accettazione silenziosa e fornisce ricette complete: chiudere gli oggetti, controllare la coercizione, serrare ogni livello di annidamento.

Intermediate~8 min di letturaAggiornato il 2026-11-02