Validazione JSON in CI: una pipeline con jsonlint, ajv e jq
In sintesi: La validazione sintattica prova che un file viene analizzato; la validazione dello schema prova che i campi rispettano il contratto. In CI ciò che conta sono codici di uscita non ambigui, posizioni degli errori e percorsi di riparazione.

In sintesi: in CI lascia che jq empty gestisca la sintassi, ajv-cli la struttura (check-jsonschema per team multi-formato) e Prettier lo stile; incatenali per livello e fermati al primo errore.
Il parser del tuo editor locale viene superato non significa che il file appartenga alla CI; la CI viene superata non significa nemmeno che i campi rispettino il contratto.
1. Separare la validazione sintattica da quella dello schema
L'espressione "validazione JSON" è usata fin troppo ampiamente, ed è per questo che i team scelgono spesso un unico strumento per risolvere due problemi diversi:
| Livello | Domanda a cui risponde | Strumenti tipici | Cosa significa un errore |
|---|---|---|---|
| Sintassi | Questo file è JSON valido? | jq empty, jsonlint, python -m json.tool |
Parentesi, virgole, virgolette o codifica sono rotte |
| Schema | Campi, tipi e proprietà obbligatorie rispettano il contratto? | ajv-cli, check-jsonschema |
Il file viene analizzato, ma la struttura di business è sbagliata |
| Stile | Indentazione e ordine delle chiavi sono coerenti? | Prettier, jq -S |
La formattazione è incoerente e i diff sono rumorosi |
Questi livelli non possono sostituirsi a vicenda. Un file che passa la sintassi può comunque fallire lo schema: {"price": "12.5"} è JSON valido, ma se lo schema richiede che price sia un numero, non è accettabile. Al contrario, per quanto completo sia lo schema, un file privo di una graffa di chiusura non lo compilerà mai.
Quindi la pipeline dovrebbe eseguire prima la sintassi, poi la struttura e infine lo stile. Fermati non appena un livello fallisce, altrimenti un errore di parsing si maschera da errore di campo.
2. Livello sintattico: jq empty è l'opzione con meno dipendenze
Se jq è già nell'immagine della CI, la validazione sintattica non costa quasi nulla:
jq empty data.json
jq empty non produce output; analizza solo il file. Esce con 0 in caso di successo e con un codice diverso da zero più le informazioni di riga/colonna in caso di errore. Questo lo rende più leggero che tirarsi dietro un intero albero di dipendenze Node, e un buon primo cancello.
Quando si validano in batch i file JSON di un repository, escludi node_modules, l'output di build e le cache di terze parti:
find . -name '*.json' \
-not -path './node_modules/*' \
-not -path './dist/*' \
-print0 | xargs -0 -n1 jq empty
-print0 e xargs -0 gestiscono gli spazi nei nomi dei file, mentre -n1 garantisce un file per invocazione così il messaggio di errore identifica il colpevole. Non passare centinaia di file a un unico processo; i messaggi di errore si sovrascriverebbero a vicenda.
jsonlint è un'altra scelta comune, di solito invocato come npx jsonlint -q data.json, dove -q stampa solo gli errori. Riporta numeri di riga e colonna, ma la sua manutenzione è meno attiva di jq, e versioni diverse tollerano BOM e commenti in modo diverso. Trattalo come un controllore di sintassi più leggibile, non come un validatore di schema.
3. Livello dello schema: le basi di ajv-cli validate
Ajv è il validatore di schema più usato nell'ecosistema JavaScript, e ajv-cli lo espone come strumento da riga di comando:
ajv validate -s schema.json -d data.json
-s punta al file dello schema e -d ai dati da validare. In caso di errore il processo esce con un codice diverso da zero, quindi la CI fallisce subito. I progetti reali di solito aggiungono due flag:
npx ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
--strict=false aiuta con gli schemi più vecchi: la modalità strict tratta le parole chiave sconosciute e le dichiarazioni required sospette come errori, cosa che molti schemi legacy non superano. --errors=text produce output leggibile per gli umani, mentre la struttura JSON predefinita è migliore per le macchine. Scegli in base a chi consuma il risultato.
Deve corrispondere anche la versione del draft dello schema. Le versioni major di Ajv supportano draft diversi per impostazione predefinita, e ajv-cli può sceglierne uno con --spec=draft7, --spec=draft2019 o --spec=draft2020. Se uno schema dichiara $schema: "https://json-schema.org/draft/2020-12/schema" ma la CLI lo compila come draft-07, alcune parole chiave vengono ignorate silenziosamente. Per saperne di più sulla stesura degli schemi e sulle differenze tra versioni, vedi Introduzione a JSON Schema.
La validazione dei formati è un'altra esigenza frequente. La parola chiave format è solo annotativa per impostazione predefinita; per far sì che email, date-time e uri impongano davvero qualcosa, carica ajv-formats:
npx ajv-cli validate -c ajv-formats -s schema.json -d data.json
Questo passaggio è facile da dimenticare, e il risultato è che un campo email accetta not-an-email. Prima di rilasciare, fornisci alla pipeline un record deliberatamente non valido; questo prova che la regola è attiva molto meglio che leggere dieci volte la documentazione.
check-jsonschema: più formati e errori più amichevoli
Se il team gestisce YAML, TOML o configurazioni di GitHub Actions oltre a JSON, check-jsonschema è più comodo:
check-jsonschema --schemafile schemas/config.json config/app.json
check-jsonschema --schemafile schemas/pipeline.json .github/workflows/ci.yml
I suoi vantaggi pratici:
- Supporto nativo per YAML, che si adatta ai file "in forma JSON ma scritti in YAML", come i manifest di Kubernetes e le configurazioni di CI.
- Gli errori includono percorso del file, numero di riga e percorso del campo, mantenendo bassi i costi di localizzazione.
- È fornito un hook pre-commit, quindi l'integrazione costa poco.
- Può recuperare gli schemi comuni da SchemaStore, ma accetta anche file locali, evitando una dipendenza dalla rete.
Nei team cross-linguaggio Python è spesso già presente, quindi installare e aggiornare check-jsonschema è più leggero che mantenere una toolchain Node. Lascia che jq gestisca la sintassi e questo strumento la struttura; la combinazione ha costi di manutenzione molto bassi.
4. I codici di uscita sono l'unico segnale affidabile per la CI
Per quanto curato sia l'output, la CI legge solo i codici di uscita. Tre trappole sono comuni:
| Sintomo | Causa | Correzione |
|---|---|---|
| Ci sono errori ma la CI è verde | Il comando termina con || true |
Rimuovi il fallback, o usalo solo dove è esplicitamente consentito |
| Nessun errore ma la CI è rossa | diff tratta "differenze trovate" come errore |
Distingui "differenza" da "errore di esecuzione" |
| L'output degli errori è troncato | L'output parallelo di più file si sovrascrive | Esegui in serie o stampa per file |
Ricorda la semantica dei codici di uscita di jq: il parsing normale restituisce 0; con -e, un risultato dell'espressione false o null restituisce 1; errori d'uso e file mancanti restituiscono 2; errori di compilazione dello script restituiscono 3. Gli strumenti di schema usano in genere 0 per il successo, 1 per l'errore di validazione e 2 per errori dello strumento o degli argomenti. Quando scrivi un wrapper, non comprimere ogni codice diverso da zero in "i dati sono sbagliati", altrimenti un errore d'ambiente verrà diagnosticato come problema di contenuto.
5. pre-commit: intercetta i problemi prima del commit
Il valore del pre-commit non è "un controllo in più"; è dare agli sviluppatori un feedback in un minuto invece che dopo un'esecuzione CI di dieci minuti. Una configurazione minima è questa:
repos:
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.0
hooks:
- id: check-jsonschema
files: ^configs/.*\.json$
args:
- --schemafile
- schemas/config.json
- repo: local
hooks:
- id: jq-syntax
name: jq syntax check
entry: jq empty
language: system
files: ^configs/.*\.json$
Due dettagli contano:
filesdovrebbe corrispondere solo alle directory che richiedono la validazione. Non eseguire gli hook sunode_moduleso sui file generati.- L'hook locale presuppone che
jqsia installato sulla macchina dello sviluppatore. Documenta quel requisito, oppure passa a un hook containerizzato.
Se il progetto usa già Husky e lint-staged, combinali direttamente:
{
"lint-staged": {
"configs/*.json": ["jq empty", "prettier --check"]
}
}
Ricorda che lint-staged passa un elenco di file in staging, quindi il comando deve accettare più percorsi. Sia jq empty sia prettier --check lo fanno. Non lasciare che un hook scansioni l'intero repository; i commit diventeranno sensibilmente più lenti.
6. GitHub Actions: una pipeline da copiare
Il workflow seguente esegue la validazione sintattica, quella dello schema e conserva il report in caso di errore:
name: json-validate
on:
push:
paths:
- 'schemas/**'
- 'fixtures/**'
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Syntax check
run: |
find fixtures -name '*.json' -print0 | xargs -0 -n1 jq empty
- name: Schema check
run: |
npx --yes ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
Il filtro paths evita che le modifiche alla sola documentazione inneschino la validazione. npx --yes si adatta ai repository senza dipendenze fissate; i progetti di produzione dovrebbero aggiungere ajv-cli a devDependencies e fissare la versione. Senza lock, un rilascio upstream che cambia il draft predefinito o la modalità strict può rendere la pipeline rossa da un giorno all'altro.
7. Localizzazione degli errori: dai numeri di riga ai percorsi dei campi
Metà del valore di un validatore è la rilevazione; l'altra metà è la localizzazione. Impara a leggere tre tipi di output:
- Errori di sintassi:
jqriporta riga e colonna, spesso con uno snippet vicino. Correggi prima parentesi e virgole; non sospettare subito la codifica. - Errori di schema:
instancePathdi Ajv punta alla posizione dei dati (per esempio/items/0/price),schemaPathpunta alla regola (per esempio#/properties/items/items/properties/price/type), ekeywordnomina il vincolo violato. - Errori di formato: quando un controllo
formatfallisce, il messaggio spesso dice solo "must match format "email"". Abbinalo ainstancePathper identificare il campo.
Incolla instancePath nel registro di indagine; è molto più utile di un muro di JSON. Per gli array, l'indice numerico ti dice anche quale record ha fallito. Se la chiave di business è più intuitiva, aggiungi dopo una riga di mappatura, come items[0].id = ORD-042.
Gli errori di schema comuni, come abusare di additionalProperties: false o scrivere rami oneOf che in realtà si sovrappongono, sono trattati in Trappole comuni nella generazione di JSON Schema. Una regola di validazione sbagliata è più pericolosa dell'assenza di validazione, perché crea un falso senso di sicurezza.
8. Tabella di scelta degli strumenti
| Esigenza | Strumento consigliato | Perché | Attenzione a |
|---|---|---|---|
| Solo sintassi | jq empty |
Zero dipendenze, veloce, codici di uscita chiari | Spazi nei nomi dei file durante l'esecuzione in batch |
| Errori riga/colonna più amichevoli | jsonlint |
Output leggibile | L'attività di manutenzione è limitata |
| Validazione JSON Schema | ajv-cli |
Ecosistema maturo, errori strutturati | Allinea le versioni dei draft e carica il supporto format |
| Repository misto JSON e YAML | check-jsonschema |
Multi-formato, amichevole con pre-commit | Fonte dello schema e accesso alla rete |
| Feedback rapido prima del commit | pre-commit / lint-staged | Elabora solo i file in staging | Gli hook devono accettare più percorsi |
| Formattazione coerente | Prettier | Ampiamente adottato | Usa --check quando non vuoi riscrivere i file |
Un'ultima raccomandazione: verifica che la pipeline fallisca davvero fornendole prima un record deliberatamente non valido. Il rischio più grande non è una configurazione complicata; è una configurazione che non entra mai in vigore. Trattare "può fallire" come criterio di accettazione è più affidabile che trattare "passa" come tale. Per la toolchain più ampia da riga di comando, continua con Editor, riga di comando e automazione.
FAQ
Devo usare jq empty o jsonlint?
Preferisci jq empty in CI: zero dipendenze, avvio veloce e semantica chiara dei codici di uscita. Usa jsonlint quando ti servono messaggi di riga/colonna più amichevoli o stai indagando in locale. Entrambi eseguono solo la validazione sintattica; nessuno dei due controlla la struttura dei campi.
Perché ajv riporta una parola chiave sconosciuta?
Per prima cosa conferma che il draft $schema dello schema corrisponda alla versione usata dalla CLI. Molti errori di "parola chiave sconosciuta" sono parole chiave di un nuovo draft incontrate da un compilatore più vecchio. Se le versioni corrispondono, cerca un errore di battitura. --strict=false può mantenere funzionanti gli schemi legacy, ma non affidarti a lungo a questa opzione per nascondere problemi reali.
La validazione dello schema passa, ma l'API fallisce comunque. Perché?
Uno schema valida la struttura, non la semantica di business. Per esempio, status può essere una stringa con un enum legale, mentre la macchina a stati vieta il passaggio diretto da paid a refunded. Regole come questa appartengono al codice o ai vincoli del database, non a JSON Schema.
Come localizzo rapidamente il campo che fallisce?
Leggi instancePath o il numero di riga dall'output dello strumento. instancePath di Ajv è un JSON Pointer che punta direttamente ai dati; check-jsonschema riporta file e riga. Registra il percorso e mappalo alla chiave di business; localizzare il problema di solito richiede pochi minuti.
Il pre-commit deve eseguire una validazione completa?
No. Il pre-commit dovrebbe elaborare solo i file in staging per mantenere veloci i commit. Le scansioni complete appartengono alla CI o ai job pianificati. La divisione è semplice: l'esecuzione locale favorisce la velocità, la CI favorisce la completezza.