JSON Toolkit

Test contrattuali JSON Schema: salda le regole di validazione nella CI

In sintesi: Uno schema non finisce quando è scritto — lì inizia il degrado. Un refuso rilassa la validazione in silenzio, un refactoring rompe tutte le fixture. Questa guida illustra un'impostazione di test contrattuali attuabile: fixture positivi, negativi e snapshot, lint dello schema stesso, compilazione una tantum riutilizzata e quattro porte CI che trasformano la rottura del contratto in una build rossa.

Intermedio~8 min di letturaPubblicato il 2026-11-05Aggiornato il 2026-11-05
Diagramma dei test contrattuali JSON Schema e delle porte di validazione CI

Conclusione: uno schema è codice, e il codice richiede test. Le fixture positive dimostrano che lascia passare ciò che deve; le negative dimostrano che blocca ciò che occorre; gli snapshot sorvegliano le modifiche involontarie. Infine ajv compile e gli snapshot con --ci fanno la ronda notturna nel tuo pipeline.

Nel momento in cui una regola di validazione si rompe, nulla solleva un errore: un allargamento dell'accettazione passa inosservato, un irrigidimento del rifiuto è pagato da tutto il team. Uno schema non è un file di configurazione — è codice che nessuno testa.

1. Lo schema si è rotto — chi lo scopre per primo

Iniziamo con una trappola realmente inciampata. Dichiara uno schema secondo la bozza 2020-12 e compila con il pacchetto predefinito di Ajv: subito parte un'eccezione:

Error: no schema with key or ref "https://json-schema.org/draft/2020-12/schema"

Il punto d'ingresso predefinito di Ajv 8 conosce solo draft-07; per il 2020-12 serve importare da ajv/dist/2020. Altra trappola: in modalità strict, scrivere format: "email" senza installare ajv-formats fa fallire anche la compilazione:

Error: unknown format "email" ignored in schema at path "#/properties/email"

Vedere questi errori in locale alla prima compilazione è buono. Il problema: molti progetti rimandano la compilazione all'arrivo della request, e lo schema rotto attende la prima vittima. Più insidioso ancora è lo schema mezzo rotto: un required scritto per distrazione come stringa invece che array fa scattare schema is invalid: data/required must be array in compilazione — ma se quel passo non è mai girato in CI, dorme in produzione.

Il primo portale quindi non valida i dati: valida lo schema stesso. Un comando:

node scripts/lint-schemas.mjs
// scripts/lint-schemas.mjs
import { readFileSync, readdirSync } from 'node:fs';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true, strict: true });
addFormats(ajv);
for (const file of readdirSync('schemas')) {
  const schema = JSON.parse(readFileSync(`schemas/${file}`, 'utf8'));
  ajv.compile(schema);
  console.log(`ok ${file}`);
}

Perché non npx ajv compile: il pacchetto ajv v8 non include una CLI, quindi npx ajv fallisce con could not determine executable to run; la CLI sta nel pacchetto separato ajv-cli (5.0.0 nella nostra misurazione). Ma ajv-cli 5 supporta solo draft-07 e draft-2019-09: applicato allo schema 2020-12 della sezione 1 solleva esattamente lo stesso no schema with key or ref "https://json-schema.org/draft/2020-12/schema". Quindi un progetto 2020-12 esegue il lint con lo stesso Ajv2020 dell'articolo; npx ajv-cli compile -s '**/*.schema.json' --strict è adatto solo a progetti draft-07.

Fallo girare come un linter: refusi, parole chiave deprecate e formati non registrati emergono già nella PR. È la prima somiglianza tra testare uno schema e testare codice comune: prima assicurati che compili.

2. Il trio: positivo, negativo, snapshot

Lo scheletro del test di schema ha tre pezzi.

Fixture positivi: campioni raccolti da risposte reali, che devono passare. Prendili dallo staging, non dalla memoria — i positivi scritti a mano coprono solo l'happy path e dimenticano null, stringhe vuote e bordi di soli campi obbligatori.

Fixture negativi: per ogni vincolo dello schema, almeno un input violante. Per minimum: 0 la coppia -1 (rifiuto) e 0 (passaggio); per enum un valore fuori lista; per additionalProperties: false un campo superfluo. I negativi contano più dei positivi: uno schema che lascia passare silenziosamente dati cattivi è un bug silenzioso che inquina fino al database.

La convenzione di nomi che trasforma il nome del file in asserzione si copia dall'open source:

schemas/order.schema.json
fixtures/order/valid-minimal.json        # deve passare
fixtures/order/valid-full.json           # deve passare
fixtures/order/invalid-type.json         # deve essere rifiutato
fixtures/order/invalid-extra-field.json  # deve essere rifiutato

Il node:test integrato in Node basta:

import { test } from 'node:test';
import { readFileSync, readdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validate = ajv.compile(
  JSON.parse(readFileSync('schemas/order.schema.json', 'utf8'))
);

for (const f of readdirSync('fixtures/order')) {
  const data = JSON.parse(readFileSync(`fixtures/order/${f}`, 'utf8'));
  if (f.startsWith('valid-')) {
    test(`${f} dovrebbe passare`, () => { assert.ok(validate(data), JSON.stringify(validate.errors)); });
  } else {
    test(`${f} dovrebbe essere rifiutato`, () => { assert.ok(!validate(data)); });
  }
}

Snapshot: serializza validate.errors in un file e confronta. Qualcuno modifica involontariamente un vincolo — lo snapshot arrossisce subito. In Jest/Vitest è una riga: expect(errors).toMatchSnapshot(); in CI passa sempre --ci: i nuovi snapshot falliscono invece di essere creati in silenzio, e la deriva non viene tollerata.

3. Raccogli tutti gli errori in un solo passaggio

Configurazione spesso dimenticata nel testare i negativi: per impostazione predefinita il validatore si ferma al primo errore. I dati sono rotti in tre punti, ne vedi uno, correggi, rilanci — e così via. Con allErrors: true (Ajv) o iter_errors (Python) vedi tutto in una volta:

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(bad), key=lambda e: list(e.absolute_path))
for e in errors:
    print(list(e.absolute_path), e.validator, e.message)

Python riserva anche un fatto controintuitivo: l'errore che validate() riporta non è "il primo" ma quello scelto da best_match. Nella nostra misurazione, con tre violazioni (tipo errato di id, role fuori lista, campo extra superfluo), è stato riportato solo additionalProperties — l'errore di tipo di id è rimasto nascosto. In CI usa sempre iter_errors per la vista completa.

E una trappola ancora più facile: format non è un'asserzione per impostazione predefinita. La bozza 2020-12 classifica format come annotazione: il jsonschema di Python non lo valida senza un format_checker passato — nel nostro test "not-an-email" è passato liberamente. Ajv richiede di installare e registrare ajv-formats. Se un formato conta (email, data, uuid), deve esistere un fixture negativo che provi che la tua configurazione del validatore lo verifica davvero.

4. Compila una volta — non usare il compilatore come monouso

Misure locali (Node v22.13.1, Ajv 8.17, stesso schema a tripla annidamento, stessi dati violanti):

Approccio Tempo per 10.000 validazioni
new Ajv() e compile ogni volta 42233 ms
Istanza a livello di modulo + compile una volta 6.9 ms

Un fattore 6000. Un validatore dentro l'handler della request che crea un nuovo Ajv a ogni chiamata è l'incidente di prestazioni più comune. Registrerei anche una convinzione errata diffusa: "ajv.validate(schema, data) ricompila ogni volta". Misurato: 100.000 chiamate in 8.2 ms — comparabile agli 11.2 ms della funzione compilata una volta, perché Ajv tiene una cache interna di schemi. Il vero male non è il cache miss, è la non riutilizzo dell'istanza. In Python è uguale: un Draft202012Validator riutilizzato è circa 5000 volte più veloce che chiamare validate() ogni volta (2000 iterazioni: 0.3 ms contro 1566 ms), perché quest'ultimo verifica lo schema stesso a ogni chiamata.

5. Salda i portali nella CI: quattro porte

Ordinati dal più economico al più caro: se i precedenti cadono, non serve lanciare i successivi:

Portale Forma del comando Cosa aggancia Costo
Lint dello schema node scripts/lint-schemas.mjs Errori di sintassi, parole chiave deprecate Millisecondi
Validazione delle fixture La suite della sezione 2 Disallineamento fixture/schema, vincoli cambiati per errore Millisecondi
Diff delle modifiche lesive json-schema-diff-validator contro main required rimosso, enum ristretto, pattern irrigidito Secondi
Validazione delle risposte reali Smoke test contro staging con asserzione .toMatchSchema() Deriva dell'endpoint reale Secondi

Configurazione minima GitHub Actions:

- run: npm ci
- run: npm run lint:schemas        # Ajv2020 lint
- run: npm run test:schema         # trio di fixture
- run: npx jest --ci --testPathPattern=schema   # guardia anti-deriva degli snapshot

Il portale delle modifiche lesive merita un accento: strumenti come json-schema-diff-validator segnalano in rosso "campo required rimosso" e "valore perso da un enum" già in revisione — proprio le due modifiche che i normali API-diff giudicano volentieri "non lesive". Togliere un campo di risposta è innocuo per un consumatore tollerante e fatale per un consumatore con switch esaustivo.

6. Asserta l'oggetto d'errore, non solo il booleano

Un test negativo che asserisce solo validate(data) === false non basta. Per lo stesso schema, assicurati che rifiuti per la ragione giusta: un vincolo estraneo può essere passato per coincidenza. L'oggetto d'errore di Ajv ha un'asimmetria famosa: quando required fallisce, instancePath punta all'oggetto padre, e il nome del campo mancante si nasconde in params.missingProperty. Misurato con id assente:

{ "instancePath": "", "schemaPath": "#/required",
  "keyword": "required", "params": { "missingProperty": "id" } }

L'asserzione si scrive "esiste un errore con keyword=required e missingProperty=id", non un vago errors.length > 0. La decifrazione completa della semantica dei campi d'errore è in Errori JSON Schema incomprensibili: localizzare gli errori Ajv e curare i messaggi.

7. Trappole frequenti — tabella rapida

Trappola Conseguenza Rimedio
Testare solo i 200 happy path La deriva delle buste d'errore passa inosservata Schema e fixture anche per le risposte d'errore
Schemi generati senza revisione required dedotti da un campione rifiutano dati reali; il team spegne la validazione Revisione umana di required/enum dopo la generazione
Uno schema globale per tutte le versioni Le differenze v1/v2 si cancellano Versionare gli schemi insieme agli endpoint
Payload interi nei log in caso di fallimento PII e segreti colano nei log CI Scrivere solo percorsi JSON Pointer e frammenti troncati
Enum congelati Una rinomina nel prodotto (canceled→cancelled) rompe la validazione Cambi di enum via revisione di compatibilità
Versioni del validatore non bloccate Comportamenti divergenti tra macchine Bloccare le versioni del validatore in CI
Sostituire i test di autorizzazione Un 403 può stare nello schema d'errore e la logica di ruolo essere comunque sbagliata Lo schema cura la struttura; l'autorizzazione si testa a parte
Dimenticare la semantica di format Indirizzi spazzatura passano liberamente Installare ajv-formats / passare format_checker, aggiungere negativi

8. Checklist di implementazione

  1. File di schema nel controllo di versione, node scripts/lint-schemas.mjs nel lint;
  2. Per ogni schema almeno 1 fixture positivo e 1 negativo per ogni vincolo; il nome del file è l'asserzione;
  3. Oggetti d'errore in snapshot; in CI passare --ci per impedire la deriva automatica;
  4. Istanze del validatore riutilizzate a livello di modulo — mai new dentro gli handler;
  5. Diff delle modifiche lesive appeso alle PR; modifiche di required/enum con revisione obbligatoria;
  6. La prima PR prova deliberatamente che il portale sa cadere: rimuovi un required, vedi il rosso, ripristina.

La questione della severità della validazione (quando chiudere additionalProperties) è un altro tema — vedi additionalProperties lasciato aperto: la validazione dello schema non serve a niente. Questo articolo parla di mantenere le regole scritte costantemente affidabili. Per le basi, parti da JSON Schema dalle basi all'ingegneria.

FAQ

Che rapporto c'è tra validazione dello schema e test contrattuali alla Pact?

È divisione del lavoro. La validazione dello schema risponde a "questo documento è strutturalmente corretto" — un'asserzione unidirezionale. Il test contrattuale guidato dal consumatore (Pact) risponde a "il provider soddisfa ancora i bisogni specifici del consumatore X" e richiede una verifica bidirezionale. I piccoli team partono con i portali dello schema e adottano Pact quando il numero di servizi cresce.

Quanti test negativi bastano?

Un negativo per vincolo è il pavimento: tante fixture negative quanti sono i vincoli (required, type, minimum, enum, pattern, additionalProperties…), più le coppie di valori limite rifiutati/accettati. Meno negativi che vincoli significa che qualche vincolo è rimasto senza sorveglianza.

Quale modifica dello schema conta come "lesiva"?

Giudica dal punto di vista del consumatore: rimuovere un campo di risposta, rimuovere o restringere un enum, irrigidire un pattern, aggiungere un campo obbligatorio alla request — tutto questo lesive; aggiungere un campo opzionale di risposta è di solito sicuro — a patto che il decoder del consumatore tolleri chiavi sconosciute. Automatizza il confronto con json-schema-diff-validator al momento della PR e decidi le zone grigie a mano.

Testare contro la vera API o i mock?

Entrambi, a strati: i test delle fixture non toccano la rete, girano in millisecondi e partono a ogni PR; la validazione reale colpisce lo staging, richiede secondi, appartiene al pipeline di smoke e salta via skipif senza credenziali. I team che in CI girano solo la vera API si logorano con rete instabile e limiti di tariffa e alla fine spengono i test.

Riferimenti

Articoli correlati

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

JSON Schema: dalle basi alla produzione

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 letturaAggiornato il 2026-10-30