Chiedi a un modello di rispondere in JSON e nella maggior parte dei casi
json.loads() funziona senza problemi. I casi che ti costano una sessione di
debug sono altri: un commento su una riga dentro l’oggetto, un code fence
markdown che lo avvolge, una chiave scritta userEmail quando il tuo codice
si aspetta user_email. Un prompt più rigido (“rispondi SOLO con l’oggetto
JSON, nessun altro testo”) rende questi casi più rari. Non li rende
impossibili, perché il modello può comunque ignorare la richiesta. Con
l’output strutturato la richiesta diventa superflua: passi all’API un JSON
Schema, che vincola il campionamento dei token del modello così che la
risposta debba rispettarlo.
JSON mode contro output strutturato: cosa garantisce ciascuno
Il “JSON mode” (response_format: {"type": "json_object"} nella vecchia API
di OpenAI, ed equivalenti sulla maggior parte degli altri provider) garantisce
una cosa sola: l’output è JSON sintatticamente valido. Non dice nulla su quali
chiavi esistono, che tipo contengono, o se un campo da cui dipendi è presente
davvero. Un modello in JSON mode può restituire {"result": "yes"} quando il
tuo codice si aspetta {"status": "approved", "confidence": 0.91}, e niente
ti avvisa che qualcosa è andato storto.
L’output strutturato (OpenAI lo chiama Structured Outputs, Anthropic lo chiama JSON outputs) è una garanzia diversa: la risposta rispetta il JSON Schema che fornisci, con le chiavi richieste presenti e il tipo corretto su ciascuna. Il function calling risolve un problema adiacente: fa sì che sia il modello a chiedere al tuo codice di eseguire qualcosa. I due si confondono facilmente perché entrambi passano uno schema. Quello che cambia è cosa lo schema modella:
| Funzione | Cosa garantisce | A cosa serve |
|---|---|---|
JSON mode (json_object) | Solo sintassi JSON valida | Fallback legacy, nessuno schema da far rispettare |
| Output strutturato (schema strict) | Il JSON rispetta esattamente il tuo schema | Dare forma alla risposta per l’utente o il tuo codice |
| Function calling / tool use | Nome del tool e argomenti conformi al suo schema | Il modello decide di invocare il tuo codice |
Una pipeline in produzione usa spesso le ultime due insieme. Il tool use strict evita che le chiamate di funzione di un agente arrivino malformate, e uno schema strict sulla risposta finale tipizza il riassunto che restituisce all’utente.
Come il constrained decoding forza la corrispondenza
Il modello non confronta mai la propria risposta con il tuo schema dopo averla generata. Lo schema cambia cosa gli è permesso generare, un token alla volta.
Il livello di serving compila il tuo JSON Schema in una grammatica: una macchina a stati che traccia, a ogni posizione dell’output, quali token porterebbero ancora a qualcosa di valido. Prima che ogni token venga campionato, ogni token del vocabolario escluso dalla grammatica in quella posizione ha il suo logit spinto a meno infinito, così ha probabilità zero dopo il softmax. Il modello campiona da quello che resta, e le probabilità relative tra i token sopravvissuti vengono mantenute. Una chiave scritta male, una parentesi graffa nel posto sbagliato, una stringa dove lo schema richiede un numero: una volta che la grammatica li esclude, quei token non esistono più come opzione.
Quella fase di compilazione ha un costo. Costruire una grammatica per un nuovo schema richiede lavoro vero, quindi la prima richiesta con quello schema è più lenta delle successive. L’API di Anthropic mette in cache la grammatica compilata per 24 ore dal suo ultimo uso, e le chiamate successive con lo stesso schema saltano la compilazione.
Il meccanismo spiega anche il limite. Il constrained decoding fissa solo la
forma, nient’altro. La grammatica sa quali token sono sintatticamente
legali, non quale sia vero, quindi il modello può comunque riempire un campo
"email" valido con un indirizzo inventato.
Come ottenere JSON conforme allo schema da OpenAI, Claude e Ollama
La Responses API di OpenAI riceve lo schema in text.format, con strict
attivo:
| |
Claude riceve lo schema in output_config.format, non in una definizione di
tool:
| |
Il JSON torna nel contenuto testuale della risposta, non in un blocco di tool call. Il vecchio workaround (definire un tool finto, forzare il modello a chiamarlo, leggere gli argomenti) funziona ancora sui modelli che supportano solo il tool use. Dove esiste l’output strutturato nativo non ne hai bisogno: il decoder fa rispettare lo schema direttamente, invece di sperare che gli argomenti del tool coincidano per caso.
Un modello eseguito in locale con Ollama
riceve lo schema tramite il campo format di /api/chat:
| |
Il meccanismo è lo stesso. Il runtime llama.cpp di Ollama costruisce una
grammatica dallo schema e maschera i token come fanno le API ospitate.
Quali funzionalità di JSON Schema supportano i provider?
Ogni provider fa rispettare un sottoinsieme di JSON Schema, e i sottoinsiemi differiscono abbastanza da rompere uno schema copiato da un’API all’altra.
OpenAI richiede che ogni oggetto porti "additionalProperties": false con
ogni sua proprietà elencata in required — non c’è modo di marcare un campo
davvero opzionale, quindi modellalo come required e nullable. Anthropic è più
permissivo: una proprietà esclusa da required è genuinamente opzionale, e le
proprietà required compaiono prima nell’output, seguite da quelle opzionali
presenti. Oltre questo, i due divergono ancora. Anthropic supporta $ref/$defs
locali, anyOf e const, ma rifiuta gli schemi ricorsivi, i limiti numerici
come minimum e maximum, e i vincoli sulla lunghezza delle stringhe come
maxLength. Gli esempi di OpenAI includono un $ref: "#" ricorsivo per
output ad albero, che la documentazione di Anthropic esclude esplicitamente.
Quando il tuo schema ha bisogno di un vincolo che il provider non supporta,
spostalo nella description del campo (“deve essere un intero positivo sotto
100”) e controllalo tu dopo il parsing. Gli helper degli SDK su entrambe le
piattaforme lo fanno automaticamente quando costruisci lo schema da un modello
Pydantic o Zod invece di scrivere JSON Schema a mano.
Come validare l’output strutturato dopo il parsing
Una forma valida non è una risposta corretta. Lo schema garantisce che
severity sia uno dei tuoi quattro valori enum. Se “blocking” sia la scelta
giusta per questo ticket, è una domanda diversa. Tratta l’oggetto parsato come
tratteresti l’invio di un form: input da validare, non verità di fatto.
| |
Instructor avvolge OpenAI, Anthropic e la
maggior parte degli altri provider in un ciclo di validazione e retry. Gli
passi un modello Pydantic. Su un fallimento di validazione rimanda l’errore al
modello e chiede di nuovo, fino a un limite di retry che imposti tu, invece di
lanciare una ValidationError al primo errore. È anche la via pratica per
ottenere una garanzia di schema su un provider o un modello più vecchio senza
strict mode nativo: il ciclo di retry sostituisce l’enforcement a livello di
decoder, e ogni errore costa un round trip in più.
I validator sui campi colgono valori malformati in una singola risposta. Per verificare se le risposte sono corrette su centinaia di input, e se una modifica al prompt le ha peggiorate, serve un eval suite.
Quando l’output strutturato fallisce comunque
Lo strict mode non copre questi casi. Controllali prima di andare a caccia di un bug nel tuo codice:
- Refusal. Un modello può rifiutarsi di rispondere, e il testo del
refusal non rispetta il tuo schema. Controlla
stop_reason(o l’equivalente) prima del parsing, non dopo che il parsing lancia un errore. - Troncamento. Raggiungere
max_tokensa metà di un oggetto produce JSON incompleto. La grammatica controlla quale token viene dopo, non quanto spazio resta per finire. Aumenta il limite o riduci lo schema. - Percorsi non vincolati. JSON mode, una libreria software di retry su un
modello senza strict mode, o un provider che ignora silenziosamente una
keyword dello schema che non supporta possono tutti restituire
"Blocking"quando il tuo enum dice"blocking". Se una richiesta nel tuo sistema passa per uno di questi percorsi, normalizza i valori in ingresso. - Dimensione dello schema. Uno schema grande con molti rami
anyOfaggiunge token a ogni richiesta, oltre al ritardo di compilazione per un nuovo schema. Mantieni gli schemi quanto più piatti e piccoli possibile per il compito.
La garanzia del provider è reale, ma il tuo codice di parsing non dovrebbe assumere che abbia tenuto.
JSON mode, schema strict o function calling: quale usare
Usa il JSON mode semplice solo con un modello o un percorso SDK senza supporto per gli schemi, come fallback e non come scelta predefinita. Usa un JSON Schema strict quando devi dare forma a una risposta per il tuo codice o per un utente: estrazione, classificazione, un riassunto tipizzato. Usa il function calling quando è il modello a decidere se eseguire qualcosa. Attiva lo strict mode anche sulla definizione del tool, così una chiamata malformata fallisce all’API invece che dentro il tuo codice.
Se tutto ciò che serve è un’etichetta da un enum breve, confronta anche un modello system one. Quando l’output è limitato a un insieme noto di risposte, un modello può scegliere invece di generare, e i compromessi su latenza e costo differiscono abbastanza che vale la pena misurarli sul tuo traffico reale.
Per tutto il resto, la scelta predefinita è uno schema strict più un validator sul risultato parsato. Lo schema mantiene corretta la forma. Verificare che il contenuto sia corretto resta compito tuo.