Structured Outputs con Claude API: JSON Affidabile al 100%
Perché gli Structured Outputs con Claude nel 2026
Chiunque abbia integrato un LLM in produzione conosce bene il problema: il modello risponde in linguaggio naturale, e tu devi estrarre dati strutturati da quel testo in modo affidabile. Regex fragili, parser JSON che esplodono su edge case, output troncati — un incubo da gestire in un sistema reale.
Nel 2026 Claude API mette a disposizione un meccanismo nativo per risolvere proprio questo problema: structured outputs. Tramite il parametro tool_use (o, nelle versioni più recenti, schemi JSON dichiarativi), puoi garantire che il modello restituisca sempre e solo dati conformi a uno schema preciso — zero parsing manuale, zero sorprese a runtime.
In questa guida vediamo tutto il percorso: dalla teoria alla produzione, con esempi reali in Python e TypeScript. Se hai già lavorato con tool use con Claude API, riconoscerai la struttura di base — ma gli structured outputs aprono un livello di controllo ancora superiore. Parleremo anche di validazione con Zod, gestione degli errori e pattern avanzati per pipeline dati.
Come Funzionano gli Structured Outputs: il Meccanismo
Claude non supporta (ancora) un parametro response_format: json_schema identico a OpenAI, ma ci sono due strategie equivalenti e molto potenti:
- Tool use con schema JSON: definisci uno “strumento” fittizio con
input_schemae forzi il modello a chiamarlo. L’output di ogni tool call è un oggetto JSON validato dall’SDK. - Prompt engineering + estrazione: chiedi esplicitamente JSON nel system prompt e poi validi tu con Pydantic o Zod. Meno robusto del primo ma utile per casi semplici.
Il pattern consigliato in produzione è il tool use forzato: imposti tool_choice: {"type": "tool", "name": "..."} e Claude è obbligato a restituire un JSON conforme allo schema che hai dichiarato. Nessuna altra scelta.
import anthropic
import json
client = anthropic.Anthropic()
# Schema che definisce l'output atteso
product_schema = {
"name": "extract_product",
"description": "Estrae le informazioni di prodotto dal testo",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Nome del prodotto"
},
"price": {
"type": "number",
"description": "Prezzo in euro"
},
"category": {
"type": "string",
"enum": ["elettronica", "abbigliamento", "casa", "sport"],
"description": "Categoria del prodotto"
},
"in_stock": {
"type": "boolean",
"description": "Disponibilita in magazzino"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tag descrittivi"
}
},
"required": ["name", "price", "category", "in_stock"]
}
}
def extract_product(text: str) -> dict:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=[product_schema],
tool_choice={"type": "tool", "name": "extract_product"},
messages=[{
"role": "user",
"content": f"Estrai le informazioni di prodotto da questo testo:\n\n{text}"
}]
)
# Il primo content block e sempre il tool_use
tool_use = response.content[0]
return tool_use.input # Gia un dict Python validato
# Test
text = (
"Il Samsung Galaxy S25 Ultra e disponibile al prezzo di 1299 euro.\n"
"E uno smartphone di fascia alta, attualmente in stock nel nostro magazzino.\n"
"Tags: flagship, 5G, fotografia, gaming."
)
result = extract_product(text)
print(json.dumps(result, indent=2, ensure_ascii=False))
L’output sarà sempre un dizionario Python conforme allo schema — se il modello non riesce a estrarre un campo required, l’intera chiamata fallisce con un errore chiaro, non silenziosamente con dati mancanti.
Structured Outputs in TypeScript con Zod
Se lavori in TypeScript, la combinazione Claude API + Zod è devastante in senso positivo: ottieni type safety a compile time e validazione a runtime in un colpo solo. Se hai già usato Zod per validare API e form, questo pattern ti sembrerà familiare ma potenziato.
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const client = new Anthropic();
// Schema Zod per la risposta
const InvoiceSchema = z.object({
invoice_number: z.string(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
vendor: z.object({
name: z.string(),
vat_id: z.string().optional(),
}),
items: z.array(
z.object({
description: z.string(),
quantity: z.number().positive(),
unit_price: z.number().nonnegative(),
total: z.number().nonnegative(),
})
),
total_amount: z.number().nonnegative(),
currency: z.enum(["EUR", "USD", "GBP"]),
});
type Invoice = z.infer<typeof InvoiceSchema>;
async function extractInvoice(rawText: string): Promise<Invoice> {
const response = await client.messages.create({
model: "claude-opus-4-5",
max_tokens: 2048,
tools: [
{
name: "extract_invoice",
description: "Estrae dati strutturati da una fattura",
input_schema: {
type: "object",
properties: {
invoice_number: { type: "string" },
date: { type: "string", description: "Formato YYYY-MM-DD" },
vendor: {
type: "object",
properties: {
name: { type: "string" },
vat_id: { type: "string" },
},
required: ["name"],
},
items: {
type: "array",
items: {
type: "object",
properties: {
description: { type: "string" },
quantity: { type: "number" },
unit_price: { type: "number" },
total: { type: "number" },
},
required: ["description", "quantity", "unit_price", "total"],
},
},
total_amount: { type: "number" },
currency: { type: "string", enum: ["EUR", "USD", "GBP"] },
},
required: [
"invoice_number", "date", "vendor", "items",
"total_amount", "currency",
],
},
},
],
tool_choice: { type: "tool", name: "extract_invoice" },
messages: [
{
role: "user",
content: `Estrai tutti i dati strutturati da questa fattura:\n\n${rawText}`,
},
],
});
const toolUse = response.content[0];
if (toolUse.type !== "tool_use") {
throw new Error("Risposta inattesa da Claude");
}
// Validazione Zod - type safe a runtime
const parsed = InvoiceSchema.safeParse(toolUse.input);
if (!parsed.success) {
throw new Error(`Dati non validi: ${parsed.error.message}`);
}
return parsed.data; // Invoice tipizzata correttamente
}
// Utilizzo
const invoiceText = [
"FATTURA N. 2026-0042",
"Data: 2026-06-15",
"Fornitore: TechSupplies SRL - P.IVA IT12345678901",
"Articoli:",
"- MacBook Pro 14\" M4 x1 @ 2199.00 EUR = 2199.00",
"- Thunderbolt Hub x2 @ 89.50 EUR = 179.00",
"Totale: 2378.00 EUR",
].join("\n");
extractInvoice(invoiceText).then((invoice) => {
console.log("Fattura estratta:", invoice);
console.log("Totale:", invoice.total_amount, invoice.currency);
});
Nota il passaggio chiave: l’output di toolUse.input viene passato a InvoiceSchema.safeParse(). Se Claude restituisce qualcosa che non corrisponde allo schema Zod (anche se rispetta quello JSON), la validazione fallisce in modo esplicito e gestibile.
Pattern Avanzato: Schema Ricorsivo e Union Types
Gli structured outputs diventano davvero potenti quando devi gestire strutture dati gerarchiche o polimorfiche. Pensa a un sistema che analizza documenti di tipi diversi: un ordine, un contratto, una email di supporto — ognuno con schema diverso. Qui il pattern con oneOf o discriminated union fa la differenza.
Questo approccio si integra perfettamente con le pipeline RAG con Claude API: prima recuperi i documenti rilevanti, poi usi structured outputs per estrarne dati uniformi da passare al tuo database.
import anthropic
from pydantic import BaseModel
from typing import Literal, Union
client = anthropic.Anthropic()
# Schema con discriminated union tramite Pydantic
class SupportTicket(BaseModel):
doc_type: Literal["support"]
ticket_id: str
priority: Literal["low", "medium", "high", "critical"]
customer_email: str
issue_summary: str
suggested_action: str
class SalesOrder(BaseModel):
doc_type: Literal["order"]
order_id: str
customer_name: str
total: float
currency: str
items_count: int
class LegalContract(BaseModel):
doc_type: Literal["contract"]
parties: list[str]
effective_date: str
contract_type: str
key_obligations: list[str]
# Tool schema che supporta tutti i tipi
classify_and_extract_schema = {
"name": "classify_and_extract",
"description": "Classifica il documento ed estrae i dati strutturati appropriati",
"input_schema": {
"type": "object",
"properties": {
"doc_type": {
"type": "string",
"enum": ["support", "order", "contract"],
"description": "Tipo di documento rilevato"
},
"ticket_id": {"type": "string"},
"priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
"customer_email": {"type": "string"},
"issue_summary": {"type": "string"},
"suggested_action": {"type": "string"},
"order_id": {"type": "string"},
"customer_name": {"type": "string"},
"total": {"type": "number"},
"currency": {"type": "string"},
"items_count": {"type": "integer"},
"parties": {"type": "array", "items": {"type": "string"}},
"effective_date": {"type": "string"},
"contract_type": {"type": "string"},
"key_obligations": {"type": "array", "items": {"type": "string"}}
},
"required": ["doc_type"]
}
}
def process_document(text: str) -> Union[SupportTicket, SalesOrder, LegalContract]:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=[classify_and_extract_schema],
tool_choice={"type": "tool", "name": "classify_and_extract"},
system="Sei un sistema di classificazione documenti. Analizza il documento,\n"
"determina il tipo e estrai SOLO i campi pertinenti al tipo rilevato.",
messages=[{"role": "user", "content": text}]
)
raw = response.content[0].input
doc_type = raw.get("doc_type")
if doc_type == "support":
return SupportTicket(**raw)
elif doc_type == "order":
return SalesOrder(**raw)
elif doc_type == "contract":
return LegalContract(**raw)
else:
raise ValueError(f"Tipo documento sconosciuto: {doc_type}")
# Test con un ticket di supporto
ticket_text = (
"Da: mario.rossi@example.com\n"
"Oggetto: Sistema di pagamento non funziona - URGENTE\n\n"
"ID ticket: TKT-2026-8891\n"
"Il checkout si blocca al pagamento con carta. Questo blocca le vendite!\n"
"Ho bisogno di una risoluzione immediata."
)
result = process_document(ticket_text)
print(f"Tipo: {result.doc_type}")
print(f"Priorita: {result.priority}")
print(f"Azione: {result.suggested_action}")
Gestione Errori e Retry con Backoff Esponenziale
In produzione gli structured outputs possono fallire per vari motivi: token limit superato, input ambiguo, o semplicemente rate limit dell’API. Un sistema robusto implementa retry intelligente con backoff esponenziale e logging strutturato.
Il principio è lo stesso che vedi in qualsiasi gestione HTTP robusta: se la chiamata fallisce, aspetti un tempo crescente prima di riprovare, con un numero massimo di tentativi. Vale la pena notare come questo si integri perfettamente con sistemi come n8n + Claude per le pipeline automatizzate.
import anthropic
import time
import logging
from typing import Callable, TypeVar
from functools import wraps
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
client = anthropic.Anthropic()
T = TypeVar("T")
def with_retry(
max_attempts: int = 3,
base_delay: float = 1.0,
max_delay: float = 60.0,
backoff_factor: float = 2.0
):
"""Decorator per retry con backoff esponenziale."""
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args, **kwargs) -> T:
last_error = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except anthropic.RateLimitError as e:
wait = min(base_delay * (backoff_factor ** attempt), max_delay)
logger.warning(
f"Rate limit (attempt {attempt+1}/{max_attempts}), retry in {wait:.1f}s"
)
time.sleep(wait)
last_error = e
except anthropic.APIError as e:
logger.error(f"API error non recuperabile: {e}")
raise
except Exception as e:
wait = min(base_delay * (backoff_factor ** attempt), max_delay)
logger.warning(f"Errore (attempt {attempt+1}/{max_attempts}): {e}, retry in {wait:.1f}s")
time.sleep(wait)
last_error = e
raise last_error
return wrapper
return decorator
# Schema per l'analisi del sentiment
sentiment_schema = {
"name": "analyze_sentiment",
"description": "Analizza il sentiment e le entita del testo",
"input_schema": {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positivo", "neutro", "negativo", "misto"]
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"topics": {
"type": "array",
"items": {"type": "string"},
"maxItems": 5
},
"key_phrases": {
"type": "array",
"items": {"type": "string"},
"maxItems": 3
}
},
"required": ["sentiment", "confidence", "topics"]
}
}
@with_retry(max_attempts=3, base_delay=2.0)
def analyze_sentiment(text: str) -> dict:
"""Analisi sentiment con retry automatico."""
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=512,
tools=[sentiment_schema],
tool_choice={"type": "tool", "name": "analyze_sentiment"},
messages=[{"role": "user", "content": f"Analizza questo testo:\n\n{text}"}]
)
result = response.content[0].input
logger.info(f"Sentiment: {result['sentiment']} (confidence: {result['confidence']:.2f})")
return result
# Batch processing
reviews = [
"Prodotto eccellente, spedizione velocissima! Super consigliato.",
"Qualita mediocre, non corrisponde alle foto. Deluso.",
"Nella media, niente di speciale ma nemmeno problemi."
]
for review in reviews:
result = analyze_sentiment(review)
print(f"Sentiment: {result['sentiment']} | Topics: {result['topics']}")
Integrazione con Database: Pipeline End-to-End
Lo scenario più comune in produzione è questo: hai una fonte di testo non strutturato (email, recensioni, PDF) e vuoi popolare un database relazionale con dati puliti. Gli structured outputs di Claude sono il ponte perfetto tra chaos e ordine.
Se stai costruendo questo tipo di pipeline su stack TypeScript, vale la pena abbinare questo approccio con Drizzle ORM per le query type-safe — la tipizzazione end-to-end dall’estrazione al database è impressionante.
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const client = new Anthropic();
const ReviewSchema = z.object({
product_name: z.string(),
rating: z.number().int().min(1).max(5),
sentiment: z.enum(["positive", "neutral", "negative"]),
pros: z.array(z.string()).max(5),
cons: z.array(z.string()).max(5),
verified_purchase: z.boolean(),
language: z.enum(["it", "en", "fr", "de", "es"]),
recommended: z.boolean(),
});
type Review = z.infer<typeof ReviewSchema>;
async function saveToDatabase(review: Review, originalText: string): Promise<void> {
// db.insert(reviews).values({ ...review, raw_text: originalText })
console.log("[DB] Inserimento:", {
product: review.product_name,
rating: review.rating,
sentiment: review.sentiment,
});
}
async function processReview(rawText: string): Promise<Review> {
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
tools: [
{
name: "normalize_review",
description: "Normalizza una recensione in formato strutturato",
input_schema: {
type: "object",
properties: {
product_name: { type: "string" },
rating: { type: "integer", minimum: 1, maximum: 5 },
sentiment: { type: "string", enum: ["positive", "neutral", "negative"] },
pros: { type: "array", items: { type: "string" } },
cons: { type: "array", items: { type: "string" } },
verified_purchase: { type: "boolean" },
language: { type: "string", enum: ["it", "en", "fr", "de", "es"] },
recommended: { type: "boolean" },
},
required: [
"product_name", "rating", "sentiment", "pros",
"cons", "verified_purchase", "language", "recommended"
],
},
},
],
tool_choice: { type: "tool", name: "normalize_review" },
system: "Sei un sistema di normalizzazione recensioni. Estrai tutte le informazioni rilevanti\n"
+ "e inferisci i campi mancanti dal contesto.",
messages: [{ role: "user", content: rawText }],
});
const toolUse = response.content[0];
if (toolUse.type !== "tool_use") throw new Error("Tool use non ricevuto");
const parsed = ReviewSchema.parse(toolUse.input);
return parsed;
}
async function processBatch(reviews: string[]): Promise<void> {
const results = await Promise.allSettled(
reviews.map(async (text) => {
const review = await processReview(text);
await saveToDatabase(review, text);
return review;
})
);
const succeeded = results.filter((r) => r.status === "fulfilled").length;
const failed = results.filter((r) => r.status === "rejected").length;
console.log(`Processate: ${succeeded} ok, ${failed} errori`);
}
processBatch([
"Acquisto verificato. iPhone 15 Pro - Fantastico! Camera eccellente, batteria migliorata. 5 stelle.",
"Sony WH-1000XM5: cancellazione rumore ottima ma scomodo dopo 2 ore. 3/5. Non raccomandato.",
]);
Ottimizzare i Costi: Scegliere il Modello Giusto
Un aspetto critico degli structured outputs in produzione è il costo per operazione. Non tutti i task richiedono Claude Opus — la scelta del modello può ridurre i costi del 90% senza perdita di qualità.
- Claude Haiku 3.5 / 4.5: estrazione semplice (sentiment, classificazione, tag), batch ad alto volume, latenza bassa
- Claude Sonnet 4.5: estrazione complessa (fatture, contratti), dati ambigui, strutture nidificate
- Claude Opus 4.5: documenti legali critici, estrazione con ragionamento complesso, quando l’errore ha costo elevato
Un altro modo per ottimizzare i costi è il prompt caching: se usi lo stesso schema e system prompt per migliaia di documenti, puoi cachare il prompt e pagare solo per i token di input variabili. Se ti interessa approfondire, ho scritto una guida specifica sul prompt caching in Claude API che copre tutti i dettagli.
🔧 Regola pratica: usa Haiku per classificazione e tag, Sonnet per estrazione dati complessi, Opus solo quando l’errore ha un costo reale di business (es. parsing contratti legali, dati finanziari critici).
FAQ e Domande Frequenti
Claude API supporta response_format: json_object come OpenAI?
No, Claude non ha un parametro response_format nativo identico a OpenAI. Il modo corretto per ottenere JSON garantito con Claude — nel 2026 — è usare il tool use forzato con tool_choice: {type: "tool", name: "..."}. Questo approccio è in realtà più robusto del semplice JSON mode di OpenAI: puoi dichiarare uno schema dettagliato con tipi, enum, array nidificati e constraint, e il modello è vincolato a rispettarlo. Anthropic ha anche annunciato supporto esplicito agli structured outputs nelle versioni recenti dei modelli claude-3.5 e claude-4.x.
Cosa succede se il testo di input non contiene i dati richiesti dallo schema?
Dipende da come hai configurato lo schema. Se un campo è in required e il testo non lo contiene, Claude tipicamente inferisce o inventa un valore plausibile — il che può essere un problema. Per evitarlo: metti in required solo i campi realmente fondamentali, usa null come tipo opzionale per i campi che potrebbero mancare ({"type": ["string", "null"]}), e aggiungi nel system prompt “Se un campo non è presente nel testo, usa null”. In questo modo ottieni un comportamento prevedibile e puoi gestire i dati mancanti a livello applicativo.
Posso usare structured outputs per streaming?
Sì, ma con qualche caveat. Quando usi stream=True con tool use, ricevi i dati del tool via input_json_delta — cioè il JSON viene costruito incrementalmente. Non hai il JSON completo finché lo stream non è terminato, quindi per validare con Pydantic/Zod devi aspettare la fine. Se hai bisogno di aggiornamenti in tempo reale durante l’estrazione — ad esempio per mostrare un progress in UI — puoi fare parsing parziale del JSON in arrivo, ma è complesso. Per la maggior parte dei casi di estrazione dati, lo streaming non aggiunge valore: aspetta la risposta completa e valida in un colpo solo.
Quanti tool posso definire in una singola chiamata?
Claude supporta fino a 64 tool per chiamata, ma per gli structured outputs il pattern ottimale è usarne uno solo con tool_choice forzato. Avere più tool in gioco quando vuoi output strutturato può creare ambiguità: il modello potrebbe chiamare il tool sbagliato o nessuno. Riserva l’uso di più tool agli scenari di agenti con strumenti reali, dove il modello deve scegliere quale azione compiere. Per l’estrazione dati, uno schema, un tool, tool_choice forzato.
Conclusione
Gli structured outputs con Claude API risolvono uno dei problemi più concreti nell’integrazione LLM in produzione: avere output JSON affidabile senza dipendere da regex fragili o parsing manuale. Il pattern tool use forzato + Pydantic/Zod ti dà garanzie che semplici prompt engineering non possono offrire.
La combinazione che funziona meglio in produzione: schema JSON dettagliato con enum e required minimi, Pydantic/Zod per validazione a runtime, scelta del modello basata sulla complessità del task, e retry con backoff per gestire i casi limite. Con questo stack puoi costruire pipeline di estrazione dati robuste che processano migliaia di documenti al giorno con errori minimi e costi controllati.
Il passo successivo naturale è combinare structured outputs con retrieval: recuperi i documenti rilevanti, estrai i dati strutturati, e li usi per alimentare la tua applicazione. Un workflow completo che vale la pena esplorare.
Suggerimenti e Risorse
🔧 Tool consigliato: usa la libreria
zod-to-json-schema(npm) opydantic-to-jsonschema(pip) per generare automaticamente gliinput_schemadai tuoi modelli esistenti — eviti di mantenere due definizioni parallele.
💡 Pro tip: per task di classificazione ad alto volume, usa
claude-haiku-4-5con schema minimale (3-5 campi). Il costo scende a meno di $0.001 per classificazione. Haiku è sorprendentemente accurato su task ben definiti.
🎯 Strategia: testa sempre il tuo schema con un set di documenti edge case prima di andare in produzione. Includi testi ambigui, testi in lingue diverse, testi vuoti e testi molto lunghi. Gli structured outputs sono robusti, ma lo schema deve essere progettato per il caso reale.

