[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-it-llm-structured-output-zod-streaming-recovery":3},{"id":4,"title":5,"author":6,"body":7,"cover":1049,"description":1050,"draft":1051,"extension":1052,"locale":1053,"meta":1054,"navigation":147,"path":1055,"publishedAt":1056,"seo":1057,"seoTitle":1058,"slug":1059,"stem":1060,"tags":1061,"__hash__":1063},"blog\u002Fit\u002Fblog\u002Fllm-structured-output-zod-streaming-recovery.md","Output strutturato da LLM con Zod: streaming, validazione, recovery","Manuel Tardivo",{"type":8,"value":9,"toc":1040},"minimark",[10,28,62,67,70,92,95,99,113,313,329,384,388,398,419,430,436,440,447,485,811,821,825,828,882,885,889,986,990,1019,1036],[11,12,13,14,18,19,22,23,27],"p",{},"Quando un LLM deve restituire JSON che la tua applicazione consuma direttamente, il fallimento più comune non è un errore HTTP: è una stringa ",[15,16,17],"strong",{},"sintatticamente plausibile"," ma ",[15,20,21],{},"semanticamente inutilizzabile"," — JSON troncato, chiavi in inglese invece che nel contratto, numeri come stringhe, array vuoti dove servono oggetti. In streaming la situazione peggiora: arrivano chunk prima che il documento sia chiudibile con ",[24,25,26],"code",{},"JSON.parse",".",[11,29,30,31,34,35,34,38,41,42,45,46,53,54,57,58,61],{},"Stack di riferimento in questo pezzo: ",[15,32,33],{},"Node.js 20+",", ",[15,36,37],{},"TypeScript 5.x",[15,39,40],{},"Zod 3.x"," (",[24,43,44],{},".safeParse","), e un client che espone stream di token (pattern comune con ",[47,48,52],"a",{"href":49,"rel":50},"https:\u002F\u002Fsdk.vercel.ai\u002Fdocs",[51],"nofollow","Vercel AI SDK"," ",[24,55,56],{},"streamText"," \u002F provider OpenAI o Anthropic documentati sul rispettivo “JSON mode” o structured output). Le API cambiano: verifica sempre la versione del pacchetto ",[24,59,60],{},"@ai-sdk\u002F*"," o del client ufficiale che usi.",[63,64,66],"h2",{"id":65},"il-problema-token-validi-ma-json-inutilizzabile","Il problema: token validi ma JSON inutilizzabile",[11,68,69],{},"Tre classi di errore ricorrenti:",[71,72,73,80,86],"ol",{},[74,75,76,79],"li",{},[15,77,78],{},"Parse error",": stream interrotto, parentesi non bilanciate, virgole mancanti.",[74,81,82,85],{},[15,83,84],{},"Schema error",": JSON valido ma non rispetta il contratto (tipo sbagliato, chiave mancante, enum fuori lista).",[74,87,88,91],{},[15,89,90],{},"Semantica tiepida",": il JSON passa Zod ma i valori sono vuoti o generici (“description”: “N\u002FA”) perché il modello ha “riempito” i campi.",[11,93,94],{},"Questo articolo copre 1 e 2 con codice. Per 3 serve validazione di business (es. lunghezza minima, vincoli incrociati tra campi) o un secondo passaggio di verifica.",[63,96,98],{"id":97},"contratto-interno-con-zod","Contratto interno con Zod",[11,100,101,102,105,106,109,110,27],{},"Definisci ",[15,103,104],{},"uno schema solo per la tua API interna",", non per il prompt grezzo del modello. Il modello può produrre un superset (campi extra): usa ",[24,107,108],{},".strip()"," in Zod o seleziona i campi con ",[24,111,112],{},".pick()",[114,115,120],"pre",{"className":116,"code":117,"language":118,"meta":119,"style":119},"language-typescript shiki shiki-themes github-light github-dark","import { z } from 'zod'\n\nexport const ExtractSchema = z.object({\n  title: z.string().min(1).max(200),\n  tags: z.array(z.string().min(1)).max(10),\n  confidence: z.number().min(0).max(1),\n})\n\nexport type Extract = z.infer\u003Ctypeof ExtractSchema>\n","typescript","",[24,121,122,142,149,175,210,244,272,278,283],{"__ignoreMap":119},[123,124,127,131,135,138],"span",{"class":125,"line":126},"line",1,[123,128,130],{"class":129},"szBVR","import",[123,132,134],{"class":133},"sVt8B"," { z } ",[123,136,137],{"class":129},"from",[123,139,141],{"class":140},"sZZnC"," 'zod'\n",[123,143,145],{"class":125,"line":144},2,[123,146,148],{"emptyLinePlaceholder":147},true,"\n",[123,150,152,155,158,162,165,168,172],{"class":125,"line":151},3,[123,153,154],{"class":129},"export",[123,156,157],{"class":129}," const",[123,159,161],{"class":160},"sj4cs"," ExtractSchema",[123,163,164],{"class":129}," =",[123,166,167],{"class":133}," z.",[123,169,171],{"class":170},"sScJk","object",[123,173,174],{"class":133},"({\n",[123,176,178,181,184,187,190,193,196,199,202,204,207],{"class":125,"line":177},4,[123,179,180],{"class":133},"  title: z.",[123,182,183],{"class":170},"string",[123,185,186],{"class":133},"().",[123,188,189],{"class":170},"min",[123,191,192],{"class":133},"(",[123,194,195],{"class":160},"1",[123,197,198],{"class":133},").",[123,200,201],{"class":170},"max",[123,203,192],{"class":133},[123,205,206],{"class":160},"200",[123,208,209],{"class":133},"),\n",[123,211,213,216,219,222,224,226,228,230,232,235,237,239,242],{"class":125,"line":212},5,[123,214,215],{"class":133},"  tags: z.",[123,217,218],{"class":170},"array",[123,220,221],{"class":133},"(z.",[123,223,183],{"class":170},[123,225,186],{"class":133},[123,227,189],{"class":170},[123,229,192],{"class":133},[123,231,195],{"class":160},[123,233,234],{"class":133},")).",[123,236,201],{"class":170},[123,238,192],{"class":133},[123,240,241],{"class":160},"10",[123,243,209],{"class":133},[123,245,247,250,253,255,257,259,262,264,266,268,270],{"class":125,"line":246},6,[123,248,249],{"class":133},"  confidence: z.",[123,251,252],{"class":170},"number",[123,254,186],{"class":133},[123,256,189],{"class":170},[123,258,192],{"class":133},[123,260,261],{"class":160},"0",[123,263,198],{"class":133},[123,265,201],{"class":170},[123,267,192],{"class":133},[123,269,195],{"class":160},[123,271,209],{"class":133},[123,273,275],{"class":125,"line":274},7,[123,276,277],{"class":133},"})\n",[123,279,281],{"class":125,"line":280},8,[123,282,148],{"emptyLinePlaceholder":147},[123,284,286,288,291,294,296,299,301,304,307,310],{"class":125,"line":285},9,[123,287,154],{"class":129},[123,289,290],{"class":129}," type",[123,292,293],{"class":170}," Extract",[123,295,164],{"class":129},[123,297,298],{"class":170}," z",[123,300,27],{"class":133},[123,302,303],{"class":170},"infer",[123,305,306],{"class":133},"\u003C",[123,308,309],{"class":129},"typeof",[123,311,312],{"class":133}," ExtractSchema>\n",[11,314,315,316,318,319,324,325,328],{},"Dopo ",[24,317,26],{}," (solo su stringa completa) usa sempre ",[15,320,321],{},[24,322,323],{},"safeParse",", mai ",[24,326,327],{},"parse"," in hot path: ti restituisce l’errore strutturato senza eccezione.",[114,330,332],{"className":116,"code":331,"language":118,"meta":119,"style":119},"const parsed = ExtractSchema.safeParse(unknownJson)\nif (!parsed.success) {\n  \u002F\u002F recovery (sezione sotto)\n}\nreturn parsed.data\n",[24,333,334,352,365,371,376],{"__ignoreMap":119},[123,335,336,339,342,344,347,349],{"class":125,"line":126},[123,337,338],{"class":129},"const",[123,340,341],{"class":160}," parsed",[123,343,164],{"class":129},[123,345,346],{"class":133}," ExtractSchema.",[123,348,323],{"class":170},[123,350,351],{"class":133},"(unknownJson)\n",[123,353,354,357,359,362],{"class":125,"line":144},[123,355,356],{"class":129},"if",[123,358,41],{"class":133},[123,360,361],{"class":129},"!",[123,363,364],{"class":133},"parsed.success) {\n",[123,366,367],{"class":125,"line":151},[123,368,370],{"class":369},"sJ8bj","  \u002F\u002F recovery (sezione sotto)\n",[123,372,373],{"class":125,"line":177},[123,374,375],{"class":133},"}\n",[123,377,378,381],{"class":125,"line":212},[123,379,380],{"class":129},"return",[123,382,383],{"class":133}," parsed.data\n",[63,385,387],{"id":386},"streaming-buffer-e-dove-fermarsi","Streaming: buffer e dove fermarsi",[11,389,390,391,397],{},"Finché streami, ",[15,392,393,394,396],{},"non chiamare ",[24,395,26],{}," su ogni delta",". Pattern minimale:",[71,399,400,403,410],{},[74,401,402],{},"Accumula testo in un buffer stringa.",[74,404,405,406,409],{},"Opzionale: se il protocollo del provider include “reasoning” o prefissi, filtra solo il blocco JSON (alcuni modelli usano fence ",[24,407,408],{},"json … ",": togli il wrapper prima del parse).",[74,411,412,413,415,416,27],{},"Solo quando chiudi lo stream (o quando un marker end-of-message arriva), prova ",[24,414,26],{}," sul buffer ",[15,417,418],{},"intero",[11,420,421,422,425,426,429],{},"Se il provider offre ",[15,423,424],{},"structured output nativo"," (object mode) che garantisce JSON valido a fine stream, semplifichi il passo 2–3 ma ",[15,427,428],{},"non"," elimini Zod: il modello può ancora violare vincoli semantici.",[11,431,432,435],{},[15,433,434],{},"Quando evitare lo streaming"," per output tabellare: se il client deve aggiornare l’UI riga per riga e il modello produce un unico grande JSON, gli aggiornamenti parziali confondono l’utente; spesso conviene streamare testo libero per l’UI e generare JSON in una seconda chiamata non streammata, oppure streammare NDJSON (una riga = un record) con schema per riga più semplice.",[63,437,439],{"id":438},"recovery-parse-fallito-o-zod-fallito","Recovery: parse fallito o Zod fallito",[11,441,442,443,446],{},"Strategia a livelli, con ",[15,444,445],{},"limite massimo di tentativi"," (es. 2 repair + 1 fallback):",[71,448,449,458,472],{},[74,450,451,454,455,457],{},[15,452,453],{},"Repair leggero",": se ",[24,456,26],{}," fallisce, prova a tagliare l’ultimo carattere non bilanciato solo se hai euristica sicura (rischioso). Spesso è più stabile inviare di nuovo al modello un prompt corto: “Questo JSON non è valido. Errore: …. Restituisci solo JSON corretto secondo lo schema.” allegando il buffer troncato.",[74,459,460,463,464,467,468,471],{},[15,461,462],{},"Zod fallito",": passa a ",[24,465,466],{},"parsed.error.flatten()"," o ",[24,469,470],{},"issues"," nel messaggio di repair così il modello sa cosa correggere.",[74,473,474,477,478,481,482,484],{},[15,475,476],{},"Degrado",": dopo N tentativi, restituisci errore business (",[24,479,480],{},"422"," verso il client) o un default sicuro (es. job in coda per revisione umana) — ",[15,483,428],{}," loop infiniti: costano token e saturano rate limit.",[114,486,488],{"className":116,"code":487,"language":118,"meta":119,"style":119},"const MAX_REPAIR = 2\n\nasync function extractWithRepair(\n  raw: string,\n  attempt: number,\n): Promise\u003CExtract> {\n  let json: unknown\n  try {\n    json = JSON.parse(raw)\n  } catch {\n    if (attempt >= MAX_REPAIR) throw new SyntaxError('Invalid JSON after repairs')\n    const fixed = await repairJsonWithLlm(raw, 'syntax')\n    return extractWithRepair(fixed, attempt + 1)\n  }\n  const r = ExtractSchema.safeParse(json)\n  if (r.success) return r.data\n  if (attempt >= MAX_REPAIR) throw new Error('Schema validation failed')\n  const fixed = await repairJsonWithLlm(raw, JSON.stringify(r.error.flatten()))\n  return extractWithRepair(fixed, attempt + 1)\n}\n",[24,489,490,502,506,520,535,547,565,578,586,604,615,649,674,693,699,717,731,758,790,806],{"__ignoreMap":119},[123,491,492,494,497,499],{"class":125,"line":126},[123,493,338],{"class":129},[123,495,496],{"class":160}," MAX_REPAIR",[123,498,164],{"class":129},[123,500,501],{"class":160}," 2\n",[123,503,504],{"class":125,"line":144},[123,505,148],{"emptyLinePlaceholder":147},[123,507,508,511,514,517],{"class":125,"line":151},[123,509,510],{"class":129},"async",[123,512,513],{"class":129}," function",[123,515,516],{"class":170}," extractWithRepair",[123,518,519],{"class":133},"(\n",[123,521,522,526,529,532],{"class":125,"line":177},[123,523,525],{"class":524},"s4XuR","  raw",[123,527,528],{"class":129},":",[123,530,531],{"class":160}," string",[123,533,534],{"class":133},",\n",[123,536,537,540,542,545],{"class":125,"line":212},[123,538,539],{"class":524},"  attempt",[123,541,528],{"class":129},[123,543,544],{"class":160}," number",[123,546,534],{"class":133},[123,548,549,552,554,557,559,562],{"class":125,"line":246},[123,550,551],{"class":133},")",[123,553,528],{"class":129},[123,555,556],{"class":170}," Promise",[123,558,306],{"class":133},[123,560,561],{"class":170},"Extract",[123,563,564],{"class":133},"> {\n",[123,566,567,570,573,575],{"class":125,"line":274},[123,568,569],{"class":129},"  let",[123,571,572],{"class":133}," json",[123,574,528],{"class":129},[123,576,577],{"class":160}," unknown\n",[123,579,580,583],{"class":125,"line":280},[123,581,582],{"class":129},"  try",[123,584,585],{"class":133}," {\n",[123,587,588,591,594,597,599,601],{"class":125,"line":285},[123,589,590],{"class":133},"    json ",[123,592,593],{"class":129},"=",[123,595,596],{"class":160}," JSON",[123,598,27],{"class":133},[123,600,327],{"class":170},[123,602,603],{"class":133},"(raw)\n",[123,605,607,610,613],{"class":125,"line":606},10,[123,608,609],{"class":133},"  } ",[123,611,612],{"class":129},"catch",[123,614,585],{"class":133},[123,616,618,621,624,627,629,632,635,638,641,643,646],{"class":125,"line":617},11,[123,619,620],{"class":129},"    if",[123,622,623],{"class":133}," (attempt ",[123,625,626],{"class":129},">=",[123,628,496],{"class":160},[123,630,631],{"class":133},") ",[123,633,634],{"class":129},"throw",[123,636,637],{"class":129}," new",[123,639,640],{"class":170}," SyntaxError",[123,642,192],{"class":133},[123,644,645],{"class":140},"'Invalid JSON after repairs'",[123,647,648],{"class":133},")\n",[123,650,652,655,658,660,663,666,669,672],{"class":125,"line":651},12,[123,653,654],{"class":129},"    const",[123,656,657],{"class":160}," fixed",[123,659,164],{"class":129},[123,661,662],{"class":129}," await",[123,664,665],{"class":170}," repairJsonWithLlm",[123,667,668],{"class":133},"(raw, ",[123,670,671],{"class":140},"'syntax'",[123,673,648],{"class":133},[123,675,677,680,682,685,688,691],{"class":125,"line":676},13,[123,678,679],{"class":129},"    return",[123,681,516],{"class":170},[123,683,684],{"class":133},"(fixed, attempt ",[123,686,687],{"class":129},"+",[123,689,690],{"class":160}," 1",[123,692,648],{"class":133},[123,694,696],{"class":125,"line":695},14,[123,697,698],{"class":133},"  }\n",[123,700,702,705,708,710,712,714],{"class":125,"line":701},15,[123,703,704],{"class":129},"  const",[123,706,707],{"class":160}," r",[123,709,164],{"class":129},[123,711,346],{"class":133},[123,713,323],{"class":170},[123,715,716],{"class":133},"(json)\n",[123,718,720,723,726,728],{"class":125,"line":719},16,[123,721,722],{"class":129},"  if",[123,724,725],{"class":133}," (r.success) ",[123,727,380],{"class":129},[123,729,730],{"class":133}," r.data\n",[123,732,734,736,738,740,742,744,746,748,751,753,756],{"class":125,"line":733},17,[123,735,722],{"class":129},[123,737,623],{"class":133},[123,739,626],{"class":129},[123,741,496],{"class":160},[123,743,631],{"class":133},[123,745,634],{"class":129},[123,747,637],{"class":129},[123,749,750],{"class":170}," Error",[123,752,192],{"class":133},[123,754,755],{"class":140},"'Schema validation failed'",[123,757,648],{"class":133},[123,759,761,763,765,767,769,771,773,776,778,781,784,787],{"class":125,"line":760},18,[123,762,704],{"class":129},[123,764,657],{"class":160},[123,766,164],{"class":129},[123,768,662],{"class":129},[123,770,665],{"class":170},[123,772,668],{"class":133},[123,774,775],{"class":160},"JSON",[123,777,27],{"class":133},[123,779,780],{"class":170},"stringify",[123,782,783],{"class":133},"(r.error.",[123,785,786],{"class":170},"flatten",[123,788,789],{"class":133},"()))\n",[123,791,793,796,798,800,802,804],{"class":125,"line":792},19,[123,794,795],{"class":129},"  return",[123,797,516],{"class":170},[123,799,684],{"class":133},[123,801,687],{"class":129},[123,803,690],{"class":160},[123,805,648],{"class":133},[123,807,809],{"class":125,"line":808},20,[123,810,375],{"class":133},[11,812,813,816,817,820],{},[24,814,815],{},"repairJsonWithLlm"," è una funzione tua: idealmente ",[15,818,819],{},"non stream",", temperatura bassa, stesso provider con costo noto.",[63,822,824],{"id":823},"telemetria-cosa-loggare-senza-pii","Telemetria: cosa loggare senza PII",[11,826,827],{},"Registra almeno:",[829,830,831,849,859,872],"ul",{},[74,832,833,836,837,840,841,844,845,848],{},[15,834,835],{},"model"," (nome versione es. ",[24,838,839],{},"gpt-4.1"," \u002F ",[24,842,843],{},"claude-3-5-sonnet-*",") e ",[15,846,847],{},"finish_reason"," se disponibile.",[74,850,851,854,855,858],{},[15,852,853],{},"Latency"," end-to-end e ",[15,856,857],{},"token"," in\u002Fout se il client li espone (OpenAI\u002FAnthropic nel response object o header dove documentato).",[74,860,861,864,865,868,869,198],{},[15,862,863],{},"Contatore repair"," e tipo fallimento (",[24,866,867],{},"syntax"," vs ",[24,870,871],{},"zod",[74,873,874,877,878,881],{},[15,875,876],{},"Hash"," (es. SHA-256) del prompt di sistema + user ",[15,879,880],{},"senza"," salvare il testo utente in chiaro se non necessario — utile per correlare regressioni quando cambi prompt.",[11,883,884],{},"Opinione esplicita: loggare il JSON grezzo in produzione è utile in staging; in prod mascheralo o campionalo, per GDPR e perché i log diventano costosi.",[63,886,888],{"id":887},"fonti-verificate-structured-outputs-json-mode-ai-sdk","Fonti verificate: Structured Outputs, JSON mode, AI SDK",[829,890,891,929,968],{},[74,892,893,896,897,901,902,905,906,909,910,913,914,919,920,924,925,928],{},[15,894,895],{},"OpenAI — Structured Outputs vs JSON mode",": la guida ufficiale distingue ",[898,899,900],"em",{},"JSON mode"," (JSON sintatticamente valido) da ",[15,903,904],{},"Structured Outputs"," con ",[24,907,908],{},"response_format"," \u002F schema: nel secondo caso il modello è costretto a rispettare lo ",[15,911,912],{},"JSON Schema"," fornito dove supportato—riduce drasticamente errori di forma rispetto al prompt “solo JSON”. Documentazione: ",[47,915,918],{"href":916,"rel":917},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fstructured-outputs",[51],"Structured model outputs"," e confronto con ",[47,921,900],{"href":922,"rel":923},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fjson-mode",[51],". ",[15,926,927],{},"Zod resta utile"," come validazione unica cross-provider, per regole di business (min\u002Fmax incrociati) e per ambienti in cui usi ancora modelli senza structured outputs.",[74,930,931,933,934,905,937,840,939,942,943,946,947,950,951,956,957,960,961,34,964,967],{},[15,932,52],{},": per generare oggetti con schema Zod lo stato dell’arte documentato è ",[24,935,936],{},"Output.object({ schema: z.object({…}) })",[24,938,56],{},[24,940,941],{},"generateText"," e stream di ",[15,944,945],{},"oggetti parziali"," tipizzati (",[24,948,949],{},"partialOutputStream","). Riferimento: ",[47,952,955],{"href":953,"rel":954},"https:\u002F\u002Fai-sdk.dev\u002Fdocs\u002Fai-sdk-core\u002Fgenerating-structured-data",[51],"Generating structured data",". Controlla la ",[15,958,959],{},"major"," installata (",[24,962,963],{},"ai",[24,965,966],{},"@ai-sdk\u002Fopenai",", …): le API evolvono rapidamente.",[74,969,970,973,974,977,978,981,982,985],{},[15,971,972],{},"Limiti reali",": anche con garanzie di schema, gestisci ",[15,975,976],{},"stream interrotti",", timeout, e ",[15,979,980],{},"rifiuti"," di sicurezza del modello; in AI SDK gli errori in streaming possono fluire come eventi di stream (",[24,983,984],{},"onError",") anziché solo eccezioni—leggi la pagina sopra per il comportamento della versione che usi.",[63,987,989],{"id":988},"sintesi-operativa","Sintesi operativa",[71,991,992,1000,1006,1012],{},[74,993,994,997,998,27],{},[15,995,996],{},"Zod dopo parse",", sempre ",[24,999,323],{},[74,1001,1002,1005],{},[15,1003,1004],{},"Stream = buffer unico",", parse solo a fine stream (salvo protocolli NDJSON dedicati).",[74,1007,1008,1011],{},[15,1009,1010],{},"Repair"," con budget finito e telemetria sui fallimenti.",[74,1013,1014,1015,1018],{},"Considera ",[15,1016,1017],{},"non-stream"," per JSON monolitici critici.",[11,1020,1021,1022,1025,1026,53,1031,1033,1034,27],{},"Riferimenti da tenere a portata di mano: documentazione OpenAI su ",[47,1023,904],{"href":916,"rel":1024},[51]," \u002F JSON mode e equivalente Anthropic, più ",[47,1027,1030],{"href":1028,"rel":1029},"https:\u002F\u002Fzod.dev",[51],"Zod",[24,1032,323],{}," e ",[24,1035,786],{},[1037,1038,1039],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":119,"searchDepth":144,"depth":144,"links":1041},[1042,1043,1044,1045,1046,1047,1048],{"id":65,"depth":144,"text":66},{"id":97,"depth":144,"text":98},{"id":386,"depth":144,"text":387},{"id":438,"depth":144,"text":439},{"id":823,"depth":144,"text":824},{"id":887,"depth":144,"text":888},{"id":988,"depth":144,"text":989},"\u002Fblog\u002F25_llm_structured_output_zod_streaming_recovery_1x.webp","Pattern TypeScript per validare output strutturato da LLM, gestire lo streaming e recuperare quando JSON o schema falliscono in produzione.",false,"md","it",{},"\u002Fit\u002Fblog\u002Fllm-structured-output-zod-streaming-recovery","2026-05-11",{"title":5,"description":1050},"Output LLM con Zod: streaming e recovery","llm-structured-output-zod-streaming-recovery","it\u002Fblog\u002Fllm-structured-output-zod-streaming-recovery",[1062],"mvp","d0HlA14vYBFoyyWsg1tKtQffrBXSejMlS20ZZJUvIYA"]