{"id":303,"date":"2026-11-08T14:33:13","date_gmt":"2026-11-08T14:33:13","guid":{"rendered":"https:\/\/devdojo.co.in\/?p=303"},"modified":"2026-11-08T14:33:13","modified_gmt":"2026-11-08T14:33:13","slug":"genai-series-part-8-structured-outputs-json","status":"publish","type":"post","link":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/","title":{"rendered":"Generative AI Part 8: Structured Outputs for Reliable JSON from LLMs"},"content":{"rendered":"\n<div class=\"dd-post\">\n<style>\n.single article:has(.dd-hero) .entry-media{display:none}\n.dd-post{--navy:#0b1020;--blue:#3178c6;--violet:#7c5cff;--ink:#1f2433;--muted:#5b6275;--line:#e3e6ef;font-size:17px;line-height:1.75;color:var(--ink)}\n.dd-post h2{margin:2.2em 0 .6em;font-size:1.6em;line-height:1.3;scroll-margin-top:90px}\n.dd-post h3{margin:1.6em 0 .5em;font-size:1.22em;scroll-margin-top:90px}\n.dd-post a{color:var(--blue);text-decoration:underline;text-underline-offset:3px}\n.dd-post pre{background:#0f1430;color:#e6e9f5;padding:18px 20px;border-radius:12px;overflow-x:auto;line-height:1.6;border:1px solid #232a52;margin:1.2em 0;font-size:14.5px !important}\n.dd-post pre code{font-size:1em !important;background:none;color:inherit;padding:0;font-family:\"JetBrains Mono\",\"Fira Code\",Consolas,Menlo,monospace;white-space:pre}\n.dd-post :not(pre)>code{background:#eef1fb;color:#2b3a8f;padding:2px 6px;border-radius:5px;font-size:.9em}\n.dd-post .dd-file{display:inline-block;background:#151b3b;color:#c9d1ff;font:600 13px\/1 \"JetBrains Mono\",Consolas,monospace;padding:7px 12px;border-radius:8px 8px 0 0;margin:1em 0 -1.2em;position:relative}\n.dd-post .dd-hero{margin:0 0 1.6em;border-radius:16px;overflow:hidden;box-shadow:0 10px 30px rgba(11,16,32,.25)}\n.dd-post .dd-hero svg{display:block;width:100%;height:auto}\n.dd-post .dd-hero .node{transition:transform .25s,filter .25s;transform-box:fill-box;transform-origin:center;cursor:pointer}\n.dd-post .dd-hero .node:hover{transform:scale(1.08);filter:drop-shadow(0 0 10px #7c5cff)}\n.dd-post .dd-series{display:inline-flex;align-items:center;gap:8px;background:#151b3b;color:#dfe3ff;border-radius:999px;padding:6px 14px;font-size:.88em;font-weight:600;margin:0 0 .8em}\n.dd-post .dd-series span{background:var(--violet);color:#fff;border-radius:999px;padding:2px 10px}\n.dd-post .dd-prereq{font-size:.96em;color:var(--muted);border-left:3px solid var(--line);padding-left:12px;margin:.6em 0 1.2em}\n.dd-post .dd-summary{background:linear-gradient(135deg,#eef3ff,#f4efff);border:1px solid #d6dcf5;border-left:5px solid var(--violet);border-radius:12px;padding:18px 22px;margin:1.4em 0}\n.dd-post .dd-summary strong.lbl{display:block;font-size:1.05em;margin-bottom:6px;color:#3a2fa0}\n.dd-post .dd-summary ul{margin:.3em 0 0 1.1em}\n.dd-post .dd-toc{background:#f8f9fc;border:1px solid var(--line);border-radius:12px;padding:16px 22px;margin:1.4em 0}\n.dd-post .dd-toc ol{margin:.4em 0 0 1.2em;columns:2;column-gap:28px}\n.dd-post .dd-toc li{break-inside:avoid;margin:.15em 0}\n@media(max-width:640px){.dd-post .dd-toc ol{columns:1}}\n.dd-post .dd-tip,.dd-post .dd-note,.dd-post .dd-warn{border-radius:10px;padding:14px 18px;margin:1.3em 0}\n.dd-post .dd-tip{background:#ecfbf3;border-left:4px solid #1f9d61}\n.dd-post .dd-note{background:#eef4ff;border-left:4px solid var(--blue)}\n.dd-post .dd-warn{background:#fff6e8;border-left:4px solid #e08a00}\n.dd-post .dd-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px;margin:1.3em 0}\n.dd-post .dd-card{border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:#fff;box-shadow:0 2px 8px rgba(20,30,70,.05)}\n.dd-post .dd-card b{display:block;color:#3a2fa0;margin-bottom:4px}\n.dd-post table{width:100%;border-collapse:collapse;margin:1.3em 0;font-size:.95em}\n.dd-post th,.dd-post td{border:1px solid var(--line);padding:10px 12px;text-align:left;vertical-align:top}\n.dd-post th{background:#151b3b;color:#fff}\n.dd-post tr:nth-child(even) td{background:#f8f9fc}\n.dd-post .dd-tablewrap{overflow-x:auto}\n.dd-post figure{margin:1.6em 0;text-align:center}\n.dd-post figure svg{width:100%;height:auto;border-radius:12px}\n.dd-post figcaption{font-size:.9em;color:var(--muted);margin-top:8px}\n.dd-post details{border:1px solid var(--line);border-radius:10px;padding:12px 16px;margin:.7em 0;background:#fff}\n.dd-post details summary{cursor:pointer;font-weight:600}\n.dd-post details[open] summary{margin-bottom:8px;color:#3a2fa0}\n.dd-post .dd-roadmap{border:2px solid #d6dcf5;background:#fbfbff}\n.dd-post .dd-roadmap h4{margin:1em 0 .3em;color:#151b3b;font-size:1em;text-transform:uppercase;letter-spacing:.04em}\n.dd-post .dd-roadmap ol{margin:.2em 0 .6em 1.4em;padding:0}\n.dd-post .dd-roadmap li{margin:.2em 0}\n.dd-post .dd-roadmap .here{background:var(--violet);color:#fff;border-radius:999px;padding:1px 9px;font-size:.8em;font-weight:700;margin-left:6px}\n.dd-post .dd-roadmap .soon{color:#8a90a6;font-size:.82em;margin-left:6px}\n.dd-post .dd-tabs{border:1px solid var(--line);border-radius:12px;padding:6px 16px 16px;margin:1.4em 0;background:#fbfbfe}\n.dd-post .dd-tabs>input{position:absolute;opacity:0;pointer-events:none}\n.dd-post .dd-tabs .labels label{display:inline-block;padding:8px 16px;margin:10px 6px 0 0;border-radius:999px;border:1px solid #cfd5ea;cursor:pointer;font-weight:600;font-size:.95em;background:#fff;transition:background .2s}\n.dd-post .dd-tabs .labels label:hover{border-color:var(--violet)}\n.dd-post .dd-tabs .panel{display:none;margin-top:12px}\n.dd-post .dd-tabs #ti1:checked~.labels label[for=ti1],.dd-post .dd-tabs #ti2:checked~.labels label[for=ti2],.dd-post .dd-tabs #ti3:checked~.labels label[for=ti3],.dd-post .dd-tabs #ti4:checked~.labels label[for=ti4]{background:var(--violet);color:#fff;border-color:var(--violet)}\n.dd-post .dd-tabs #ti1:checked~.p1,.dd-post .dd-tabs #ti2:checked~.p2,.dd-post .dd-tabs #ti3:checked~.p3,.dd-post .dd-tabs #ti4:checked~.p4{display:block}\n.dd-post .dd-tabs #ti1:focus-visible~.labels label[for=ti1],.dd-post .dd-tabs #ti2:focus-visible~.labels label[for=ti2],.dd-post .dd-tabs #ti3:focus-visible~.labels label[for=ti3],.dd-post .dd-tabs #ti4:focus-visible~.labels label[for=ti4]{outline:2px solid var(--blue);outline-offset:2px}\n.dd-post .dd-cta{background:linear-gradient(135deg,#0b1020,#151b3b);color:#e8ebff;border-radius:16px;padding:24px 26px;margin:2em 0;text-align:center}\n.dd-post .dd-cta h3{color:#fff;margin-top:0}\n.dd-post .dd-cta a{color:#a99bff}\n.dd-post .dd-vsc{position:relative;margin:1.5em 0;border-radius:12px;overflow:hidden;background:#1e1e1e;border:1px solid #2b2b2b;box-shadow:0 14px 34px rgba(0,0,0,.28);font-family:\"Segoe UI\",system-ui,sans-serif}\n.dd-post .dd-vsc>input{position:absolute;opacity:0;pointer-events:none}\n.dd-post .dd-vsc-bar{display:flex;align-items:center;gap:12px;background:#323233;color:#cccccc;font-size:12.5px;padding:8px 12px}\n.dd-post .dd-dots{display:inline-flex;gap:7px}.dd-post .dd-dots i{width:12px;height:12px;border-radius:50%;background:#ff5f57;display:block}.dd-post .dd-dots i:nth-child(2){background:#febc2e}.dd-post .dd-dots i:nth-child(3){background:#28c840}\n.dd-post .dd-vsc-title{flex:1;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:50px;opacity:.85}\n.dd-post .dd-vsc-tabs{display:flex;align-items:stretch;background:#252526;border-bottom:1px solid #1e1e1e;overflow-x:auto}\n.dd-post .dd-vsc-tabs label{display:flex;align-items:center;gap:7px;padding:9px 16px;color:#969696;font-size:13px;cursor:pointer;background:#2d2d2d;border-right:1px solid #252526;border-top:1px solid transparent;white-space:nowrap;margin:0;transition:color .15s}\n.dd-post .dd-vsc-tabs label:hover{color:#e0e0e0}\n.dd-post .dd-vsc-tabs .ic{font:700 9.5px\/1 Consolas,monospace;padding:3px 4px;border-radius:3px;color:#1e1e1e}\n.dd-post .ic.py{background:#ffd43b}.dd-post .ic.js{background:#f1e05a}.dd-post .ic.sh{background:#89e051}\n.dd-post .dd-vsc-tabs label.run{margin-left:auto;background:transparent;border:0;color:#89d185;font-weight:600;display:none}\n.dd-post .dd-vsc-tabs label.run:hover{color:#b5f0b0;background:#2a2d2e}\n.dd-post .dd-vsc-pane{display:none}\n.dd-post .dd-vsc>.f1:checked~.dd-vsc-tabs .t1,.dd-post .dd-vsc>.f2:checked~.dd-vsc-tabs .t2,.dd-post .dd-vsc>.f3:checked~.dd-vsc-tabs .t3{background:#1e1e1e;color:#ffffff;border-top-color:#3178c6}\n.dd-post .dd-vsc>.f1:checked~.p1,.dd-post .dd-vsc>.f2:checked~.p2,.dd-post .dd-vsc>.f3:checked~.p3{display:block}\n.dd-post .dd-vsc>.f1:checked~.dd-vsc-tabs .run1,.dd-post .dd-vsc>.f2:checked~.dd-vsc-tabs .run2,.dd-post .dd-vsc>.f3:checked~.dd-vsc-tabs .run3{display:flex}\n.dd-post .dd-vsc>.f1:focus-visible~.dd-vsc-tabs .t1,.dd-post .dd-vsc>.f2:focus-visible~.dd-vsc-tabs .t2,.dd-post .dd-vsc>.f3:focus-visible~.dd-vsc-tabs .t3,.dd-post .dd-vsc>input[type=checkbox]:focus-visible~.dd-vsc-tabs .run{outline:2px solid #3178c6;outline-offset:-2px}\n.dd-post .dd-vsc-crumbs{background:#1e1e1e;color:#a9a9a9;font-size:12px;padding:5px 16px 2px}\n.dd-post .dd-vsc pre.dd-vsc-code{background:#1e1e1e;color:#d4d4d4;border:0;border-radius:0;margin:0;padding:8px 0 14px;counter-reset:ln;line-height:1.62;overflow-x:auto}\n.dd-post .dd-vsc pre code{font-family:Consolas,\"JetBrains Mono\",\"Fira Code\",Menlo,monospace;color:inherit;background:none;white-space:pre}\n.dd-post .dd-vsc .l{display:inline-block;min-width:100%;padding-right:18px;box-sizing:border-box}\n.dd-post .dd-vsc .l::before{counter-increment:ln;content:counter(ln);display:inline-block;width:3.2em;padding-right:1.4em;text-align:right;color:#6e7681;user-select:none;-webkit-user-select:none}\n.dd-post .dd-vsc .l:hover{background:#2a2d2e}.dd-post .dd-vsc .l:hover::before{color:#c6c6c6}\n.dd-post .dd-vsc .k,.dd-post .dd-vsc .kn{color:#c586c0}.dd-post .dd-vsc .kc{color:#569cd6}.dd-post .dd-vsc .s{color:#ce9178}.dd-post .dd-vsc .c{color:#6a9955}.dd-post .dd-vsc .m{color:#b5cea8}.dd-post .dd-vsc .nf{color:#dcdcaa}.dd-post .dd-vsc .nc{color:#4ec9b0}.dd-post .dd-vsc .nb{color:#4ec9b0}.dd-post .dd-vsc .nd{color:#dcdcaa}.dd-post .dd-vsc .n{color:#9cdcfe}.dd-post .dd-vsc .o{color:#d4d4d4}.dd-post .dd-vsc .gp{color:#89d185;user-select:none;-webkit-user-select:none}\n.dd-post .dd-vsc-term{display:none;border-top:1px solid #3c3c3c;background:#181818}\n.dd-post .dd-vsc>.r1:checked~.p1 .dd-vsc-term,.dd-post .dd-vsc>.r2:checked~.p2 .dd-vsc-term,.dd-post .dd-vsc>.r3:checked~.p3 .dd-vsc-term{display:block;animation:ddterm .35s ease-out}\n@keyframes ddterm{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}\n.dd-post .dd-term-head{display:flex;gap:18px;align-items:center;font-size:11px;letter-spacing:.06em;color:#8b8b8b;padding:7px 16px 0}\n.dd-post .dd-term-head span:first-child{color:#e7e7e7;border-bottom:1px solid #e7e7e7;padding-bottom:4px}\n.dd-post .dd-term-head .dd-term-note{margin-left:auto;letter-spacing:0;text-transform:none;color:#d7ba7d;border:0;font-size:11.5px}\n.dd-post .dd-vsc-term pre{background:#181818;color:#cccccc;border:0;margin:0;padding:10px 16px 14px;border-radius:0}\n.dd-post .dd-vsc-status{display:flex;flex-wrap:wrap;gap:4px 14px;background:#007acc;color:#fff;font-size:11.5px;padding:4px 12px}\n.dd-post .dd-vsc-status .sp{flex:1}\n.dd-post .dd-vsc-termonly pre.dd-vsc-code{background:#181818;padding:12px 16px}\n@media(max-width:640px){.dd-post .dd-vsc-title{padding-right:0;text-align:left}.dd-post .dd-vsc-status span:nth-child(n+4){display:none}.dd-post .dd-vsc .l::before{width:2.2em;padding-right:1em}}\n.dd-post .dd-vsc-hint{font-size:.93em;color:var(--muted);margin-top:-.6em}\n.dd-post .dd-next{border:1px dashed var(--violet);border-radius:12px;padding:16px 20px;margin:1.6em 0;background:#f7f4ff}\n<\/style>\n<style>\n.dd-post .ic.txt{background:#9da5b4}\n.dd-post .dd-levels{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin:.6em 0}\n.dd-post .dd-levels div{background:#151b3b;color:#e8ebff;border-radius:10px;padding:10px 12px;font-size:.92em}\n.dd-post .dd-levels b{display:block;color:#a99bff;font-size:.82em;text-transform:uppercase;letter-spacing:.05em}\n.dd-post .dd-meter{height:10px;border-radius:999px;background:#2a3266;margin-top:8px;overflow:hidden}\n.dd-post .dd-meter i{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,#3178c6,#7c5cff)}\n<\/style>\n\n<div class=\"dd-hero\">\n<svg viewBox=\"0 0 1200 520\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Generative AI series Part 8 banner: messy free text from an LLM passes through a JSON Schema built with Pydantic or Zod and comes out as clean, valid JSON that your code can trust\">\n<defs>\n<linearGradient id=\"ddbg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#0b1020\"\/><stop offset=\"1\" stop-color=\"#151b3b\"\/><\/linearGradient>\n<linearGradient id=\"ddac\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#3178c6\"\/><stop offset=\"1\" stop-color=\"#7c5cff\"\/><\/linearGradient>\n<radialGradient id=\"ddglow\" cx=\".5\" cy=\".5\" r=\".5\"><stop offset=\"0\" stop-color=\"#7c5cff\" stop-opacity=\".45\"\/><stop offset=\"1\" stop-color=\"#7c5cff\" stop-opacity=\"0\"\/><\/radialGradient>\n<\/defs>\n<rect x=\"0\" y=\"-60\" width=\"1200\" height=\"640\" fill=\"url(#ddbg)\"\/>\n<circle cx=\"1000\" cy=\"110\" r=\"250\" fill=\"url(#ddglow)\"\/>\n<g opacity=\".12\" stroke=\"#9fb2ff\"><path d=\"M0 100H1200M0 200H1200M0 300H1200M0 400H1200M200 0V520M400 0V520M600 0V520M800 0V520M1000 0V520\"\/><\/g>\n<rect x=\"60\" y=\"46\" width=\"128\" height=\"38\" rx=\"19\" fill=\"url(#ddac)\"\/>\n<text x=\"124\" y=\"71\" text-anchor=\"middle\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"18\" font-weight=\"700\" fill=\"#fff\">PART 8<\/text>\n<text x=\"204\" y=\"71\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"18\" fill=\"#aab4e8\">Generative AI Series \u00b7 DevDojo<\/text>\n<text x=\"60\" y=\"148\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"54\" font-weight=\"800\" fill=\"#ffffff\">Structured Outputs<\/text>\n<text x=\"60\" y=\"206\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"44\" font-weight=\"800\" fill=\"url(#ddac)\">Reliable JSON from LLMs<\/text>\n<text x=\"60\" y=\"248\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"23\" fill=\"#c9d1ff\">Pydantic + Zod schemas \u00b7 OpenAI \u00b7 Ollama<\/text>\n<g font-family=\"Segoe UI,Arial,sans-serif\">\n<g class=\"node\"><rect x=\"60\" y=\"290\" width=\"300\" height=\"190\" rx=\"16\" fill=\"#1b2250\" stroke=\"#ff7b72\" stroke-width=\"2\"\/><text x=\"210\" y=\"320\" text-anchor=\"middle\" font-size=\"15\" fill=\"#ffb3ad\">Free text reply<\/text><text x=\"80\" y=\"355\" font-size=\"15\" fill=\"#e8ebff\">Sure! Here is the data:<\/text><text x=\"80\" y=\"383\" font-size=\"15\" fill=\"#e8ebff\">&#8220;`json {&#8220;age&#8221;: &#8220;twenty&#8221;,<\/text><text x=\"80\" y=\"411\" font-size=\"15\" fill=\"#e8ebff\">&#8220;skills&#8221;: [&#8230;],} &#8220;`<\/text><text x=\"80\" y=\"447\" font-size=\"15\" fill=\"#ff7b72\">\u2717 JSON.parse fails<\/text><\/g>\n<g class=\"node\"><rect x=\"480\" y=\"320\" width=\"240\" height=\"130\" rx=\"20\" fill=\"url(#ddac)\"\/><text x=\"600\" y=\"365\" text-anchor=\"middle\" font-size=\"26\" font-weight=\"800\" fill=\"#fff\">JSON Schema<\/text><text x=\"600\" y=\"395\" text-anchor=\"middle\" font-size=\"16\" fill=\"#eef\">Pydantic \u00b7 Zod<\/text><text x=\"600\" y=\"422\" text-anchor=\"middle\" font-size=\"14\" fill=\"#dfe3ff\">types \u00b7 enums \u00b7 limits<\/text><\/g>\n<g class=\"node\"><rect x=\"840\" y=\"290\" width=\"300\" height=\"190\" rx=\"16\" fill=\"#0b1020\" stroke=\"#89e051\" stroke-width=\"2\"\/><text x=\"990\" y=\"320\" text-anchor=\"middle\" font-size=\"15\" fill=\"#b8f0a0\">Valid JSON<\/text><text x=\"862\" y=\"352\" font-family=\"Consolas,monospace\" font-size=\"15\" fill=\"#9cdcfe\">{ &#8220;category&#8221;: <tspan fill=\"#ce9178\">&#8220;billing&#8221;<\/tspan>,<\/text><text x=\"862\" y=\"378\" font-family=\"Consolas,monospace\" font-size=\"15\" fill=\"#9cdcfe\">  &#8220;priority&#8221;: <tspan fill=\"#b5cea8\">5<\/tspan>,<\/text><text x=\"862\" y=\"404\" font-family=\"Consolas,monospace\" font-size=\"15\" fill=\"#9cdcfe\">  &#8220;order_id&#8221;: <tspan fill=\"#ce9178\">&#8220;A-4821&#8221;<\/tspan> }<\/text><text x=\"862\" y=\"447\" font-size=\"15\" fill=\"#89e051\">\u2713 ready for your code<\/text><\/g>\n<\/g>\n<path id=\"ddf1\" d=\"M360 385 C 420 385, 420 385, 480 385\" fill=\"none\" stroke=\"url(#ddac)\" stroke-width=\"3\" stroke-dasharray=\"8 8\" opacity=\".8\"\/>\n<path id=\"ddf2\" d=\"M720 385 C 780 385, 780 385, 840 385\" fill=\"none\" stroke=\"url(#ddac)\" stroke-width=\"3\" stroke-dasharray=\"8 8\" opacity=\".8\"\/>\n<circle r=\"6\" fill=\"#ff7b72\"><animateMotion dur=\"1.4s\" repeatCount=\"indefinite\"><mpath href=\"#ddf1\"\/><\/animateMotion><\/circle>\n<circle r=\"5\" fill=\"#ffffff\"><animateMotion dur=\"1.4s\" begin=\".7s\" repeatCount=\"indefinite\"><mpath href=\"#ddf1\"\/><\/animateMotion><\/circle>\n<circle r=\"6\" fill=\"#89e051\"><animateMotion dur=\"1.4s\" begin=\".35s\" repeatCount=\"indefinite\"><mpath href=\"#ddf2\"\/><\/animateMotion><\/circle>\n<circle r=\"5\" fill=\"#61dafb\"><animateMotion dur=\"1.4s\" begin=\"1.05s\" repeatCount=\"indefinite\"><mpath href=\"#ddf2\"\/><\/animateMotion><\/circle>\n<text x=\"60\" y=\"510\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"16\" fill=\"#7f89b5\">devdojo.co.in \u00b7 Beginner friendly \u00b7 Python + Node.js examples<\/text>\n<\/svg>\n<\/div>\n\n<div class=\"dd-series\">Series: Generative AI <span>Part 8 of 30<\/span><\/div>\n<p class=\"dd-prereq\"><strong>What you should know first:<\/strong> read <a href=\"https:\/\/devdojo.co.in\/?p=284\">Part 7: Stream LLM Responses and Build a Chat UI<\/a> and <a href=\"https:\/\/devdojo.co.in\/?p=223\">Part 3: Your First LLM API Calls<\/a>. You need Python 3.10+ or Node.js 20.6+, and either an OpenAI API key or a free local model in <a href=\"https:\/\/devdojo.co.in\/?p=281\">Ollama (Part 6)<\/a>.<\/p>\n\n<p>Chat answers are made for people. But the moment your <em>code<\/em> needs to use an answer \u2014 save it to a database, show it in a form, send it to another API \u2014 you need data, not a friendly paragraph. In this part you will learn <strong>structured outputs<\/strong>: how to make an LLM return <strong>reliable JSON<\/strong> that always matches a shape you define. You will see why &#8220;please reply in JSON&#8221; breaks, describe the shape once with <strong>Pydantic<\/strong> (Python) or <strong>Zod<\/strong> (Node.js), get typed objects back from OpenAI and from a local Ollama model, and add a validate-and-retry safety net for the rare cases that still go wrong.<\/p>\n\n<div class=\"dd-summary\">\n<strong class=\"lbl\">Quick Summary<\/strong>\n<ul>\n<li><strong>Structured outputs<\/strong> means the model&#8217;s reply is forced to follow a <strong>JSON Schema<\/strong> you give it: the right keys, the right types, only the allowed values.<\/li>\n<li>Only <em>asking<\/em> for JSON in the prompt is not enough. Models add extra text, code fences or trailing commas, and <code>JSON.parse<\/code> fails.<\/li>\n<li>Describe the shape once with a <strong>Pydantic<\/strong> model or a <strong>Zod<\/strong> schema. The SDK turns it into JSON Schema for you.<\/li>\n<li>OpenAI: <code>client.responses.parse(..., text_format=Ticket)<\/code> in Python or <code>zodTextFormat(Ticket, \"ticket\")<\/code> in Node.js gives you <code>output_parsed<\/code>.<\/li>\n<li>Ollama: pass the schema as <code>format<\/code>, then validate the reply with <code>model_validate_json<\/code> or <code>Ticket.parse<\/code>.<\/li>\n<li>Always validate, and handle refusals, cut-off answers and wrong-but-valid values with a small retry loop.<\/li>\n<\/ul>\n<\/div>\n\n<nav class=\"dd-toc\" aria-label=\"Table of contents\">\n<strong>In this post<\/strong>\n<ol>\n<li><a href=\"#what-are-structured-outputs\">What are structured outputs?<\/a><\/li>\n<li><a href=\"#why-prompt-json-fails\">Why &#8220;reply in JSON&#8221; fails<\/a><\/li>\n<li><a href=\"#three-levels\">Prompt vs JSON mode vs schema<\/a><\/li>\n<li><a href=\"#define-schema\">Define the shape with Pydantic and Zod<\/a><\/li>\n<li><a href=\"#openai\">Structured outputs with OpenAI<\/a><\/li>\n<li><a href=\"#ollama\">Structured outputs with Ollama<\/a><\/li>\n<li><a href=\"#validate-retry\">Validate and retry<\/a><\/li>\n<li><a href=\"#schema-rules\">Schema rules to remember<\/a><\/li>\n<li><a href=\"#errors\">Common errors and fixes<\/a><\/li>\n<li><a href=\"#best-practices\">Best practices<\/a><\/li>\n<li><a href=\"#practice\">Practice task<\/a><\/li>\n<li><a href=\"#faq\">FAQ<\/a><\/li>\n<\/ol>\n<\/nav>\n\n<details class=\"dd-roadmap\">\n<summary>Full Generative AI roadmap (30 parts)<\/summary>\n<h4>Beginner<\/h4>\n<ol start=\"1\">\n<li><a href=\"https:\/\/devdojo.co.in\/?p=215\">What Is Generative AI and How Do LLMs Work?<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=219\">Tokens, Context Windows and Model Parameters<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=223\">Your First LLM API Calls in Python and Node.js (OpenAI, Anthropic, Gemini)<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=240\">Prompt Engineering Basics: Roles, Instructions and Examples<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=244\">Temperature, Top-p and Controlling Model Output<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=281\">Run LLMs Locally with Ollama<\/a><\/li>\n<li><a href=\"https:\/\/devdojo.co.in\/?p=284\">Streaming Responses and Building a Simple Chat UI<\/a><\/li>\n<li>Structured Outputs: Getting Reliable JSON from LLMs <span class=\"here\">You are here<\/span><\/li>\n<\/ol>\n<h4>Intermediate<\/h4>\n<ol start=\"9\">\n<li>Embeddings Explained: Turning Text into Vectors <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Vector Databases: pgvector, Chroma and Pinecone <span class=\"soon\">Coming soon<\/span><\/li>\n<li>RAG Fundamentals: Retrieval-Augmented Generation <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Document Loading and Chunking Strategies <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Better RAG: Hybrid Search, Re-ranking and Query Rewriting <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Function Calling and Tool Use <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Conversation Memory and Long Context Management <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Multimodal AI: Vision, Images and Audio Inputs <span class=\"soon\">Coming soon<\/span><\/li>\n<li>How Image Generation Works: Diffusion Models Basics <span class=\"soon\">Coming soon<\/span><\/li>\n<li>LangChain vs LlamaIndex: When to Use a Framework <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Testing LLM Apps: Evals, Metrics and Test Sets <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Cutting Cost and Latency: Caching, Batching and Model Choice <span class=\"soon\">Coming soon<\/span><\/li>\n<\/ol>\n<h4>Advanced<\/h4>\n<ol start=\"21\">\n<li>Fine-Tuning vs Prompting vs RAG: Choosing the Right Tool <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Fine-Tuning an Open Model with LoRA <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Transformers Deep Dive: Attention Explained Simply <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Reasoning Models and Thinking Time <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Guardrails, Safety and Prompt Injection Defence <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Monitoring and Tracing LLM Apps in Production <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Serving Open Models: vLLM, Quantization and GPUs <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Building a Production GenAI App End to End <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Responsible AI: Privacy, Licensing and Bias <span class=\"soon\">Coming soon<\/span><\/li>\n<li>Final Project: Build Your Own AI Knowledge Assistant <span class=\"soon\">Coming soon<\/span><\/li>\n<\/ol>\n<\/details>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"what-are-structured-outputs\">What are structured outputs?<\/h2>\n<p>Normally an LLM can write <em>any<\/em> text it likes. <strong>Structured outputs<\/strong> is a feature that limits the model to text that fits a <strong>JSON Schema<\/strong> \u2014 a small description of the data you want: which keys, which types, which values are allowed and which keys are required. The schema works like a form with fixed boxes. The model can only fill in the boxes; it cannot add a &#8220;Sure! Here you go&#8221; line or invent a new box.<\/p>\n<p>How is that possible? Remember from <a href=\"https:\/\/devdojo.co.in\/?p=219\">Part 2<\/a> that a model writes one token at a time, choosing from many possible next tokens. With structured outputs, the server (OpenAI, Ollama and others) checks every candidate token against the schema and <strong>blocks the tokens that would break it<\/strong>. This is called <em>constrained decoding<\/em>. If the schema says <code>\"priority\"<\/code> is an integer, a letter simply cannot be picked there.<\/p>\n\n<figure>\n<svg viewBox=\"0 0 1000 300\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Constrained decoding: after the text priority colon, the model wants to write the word high or the number 5; the schema blocks the word and only allows digits, so the model writes 5\">\n<rect width=\"1000\" height=\"300\" rx=\"14\" fill=\"#0f1430\"\/>\n<g font-family=\"Segoe UI,Arial,sans-serif\">\n<text x=\"30\" y=\"42\" font-size=\"18\" font-weight=\"700\" fill=\"#c9d1ff\">Constrained decoding: the schema filters each next token<\/text>\n<rect x=\"30\" y=\"80\" width=\"300\" height=\"56\" rx=\"10\" fill=\"#1b2250\" stroke=\"#3178c6\"\/>\n<text x=\"48\" y=\"114\" font-family=\"Consolas,monospace\" font-size=\"18\" fill=\"#9cdcfe\">{&#8220;priority&#8221;: <tspan fill=\"#d4d4d4\">\u258d<\/tspan><\/text>\n<text x=\"30\" y=\"170\" font-size=\"15\" fill=\"#aab4e8\">Text written so far<\/text>\n<path d=\"M330 108 H 420\" stroke=\"#7c5cff\" stroke-width=\"3\" marker-end=\"url(#ar)\"\/>\n<defs><marker id=\"ar\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto\"><path d=\"M0 0L10 5L0 10z\" fill=\"#7c5cff\"\/><\/marker><\/defs>\n<text x=\"440\" y=\"70\" font-size=\"15\" fill=\"#aab4e8\">Candidate next tokens<\/text>\n<rect x=\"440\" y=\"84\" width=\"140\" height=\"40\" rx=\"8\" fill=\"#3a1d2a\" stroke=\"#ff7b72\"\/><text x=\"510\" y=\"110\" text-anchor=\"middle\" font-family=\"Consolas,monospace\" font-size=\"17\" fill=\"#ffb3ad\">&#8220;high&#8221; \u2717<\/text>\n<rect x=\"440\" y=\"134\" width=\"140\" height=\"40\" rx=\"8\" fill=\"#3a1d2a\" stroke=\"#ff7b72\"\/><text x=\"510\" y=\"160\" text-anchor=\"middle\" font-family=\"Consolas,monospace\" font-size=\"17\" fill=\"#ffb3ad\">null \u2717<\/text>\n<rect x=\"600\" y=\"84\" width=\"140\" height=\"40\" rx=\"8\" fill=\"#16331f\" stroke=\"#89e051\"\/><text x=\"670\" y=\"110\" text-anchor=\"middle\" font-family=\"Consolas,monospace\" font-size=\"17\" fill=\"#b8f0a0\">5 \u2713<\/text>\n<rect x=\"600\" y=\"134\" width=\"140\" height=\"40\" rx=\"8\" fill=\"#16331f\" stroke=\"#89e051\"\/><text x=\"670\" y=\"160\" text-anchor=\"middle\" font-family=\"Consolas,monospace\" font-size=\"17\" fill=\"#b8f0a0\">3 \u2713<\/text>\n<rect x=\"770\" y=\"84\" width=\"200\" height=\"90\" rx=\"10\" fill=\"url(#ddac2)\"\/>\n<defs><linearGradient id=\"ddac2\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#3178c6\"\/><stop offset=\"1\" stop-color=\"#7c5cff\"\/><\/linearGradient><\/defs>\n<text x=\"870\" y=\"118\" text-anchor=\"middle\" font-size=\"16\" font-weight=\"700\" fill=\"#fff\">Schema says:<\/text>\n<text x=\"870\" y=\"146\" text-anchor=\"middle\" font-family=\"Consolas,monospace\" font-size=\"15\" fill=\"#fff\">integer, 1 to 5<\/text>\n<rect x=\"30\" y=\"210\" width=\"940\" height=\"60\" rx=\"10\" fill=\"#151b3b\"\/>\n<text x=\"50\" y=\"246\" font-size=\"16\" fill=\"#e8ebff\">Result: the model can only pick tokens that keep the JSON valid, so the reply always parses and has the right types.<\/text>\n<\/g>\n<\/svg>\n<figcaption>Constrained decoding: tokens that would break the schema are blocked before the model can choose them.<\/figcaption>\n<\/figure>\n\n<p>Real projects use this all the time: pulling fields out of emails and invoices, sorting support tickets, turning a resume into a profile, creating quiz questions for an app, or deciding which button a chatbot should press. Anywhere the next step is <strong>code<\/strong>, you want structured outputs.<\/p>\n\n<h2 id=\"why-prompt-json-fails\">Why &#8220;please reply in JSON&#8221; fails<\/h2>\n<p>The first idea everyone tries is to write &#8220;Reply only in JSON&#8221; in the prompt. It works most of the time \u2014 and &#8220;most of the time&#8221; is the problem. Here is a very typical reply from a chat model, and what happens when your code tries to read it. These two files run without any API key:<\/p>\n\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: naive_json.py, naive-json.mjs\">\n<input type=\"radio\" name=\"v1\" id=\"v1f1\" class=\"f1\" checked>\n<input type=\"radio\" name=\"v1\" id=\"v1f2\" class=\"f2\">\n<input type=\"checkbox\" id=\"v1r1\" class=\"r1\">\n<input type=\"checkbox\" id=\"v1r2\" class=\"r2\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">naive_json.py \u2014 devdojo-json \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v1f1\" class=\"t1\"><b class=\"ic py\">PY<\/b>naive_json.py<\/label>\n<label for=\"v1f2\" class=\"t2\"><b class=\"ic js\">JS<\/b>naive-json.mjs<\/label>\n<label for=\"v1r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<label for=\"v1r2\" class=\"run run2\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a naive_json.py<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"kn\">import<\/span> <span class=\"n\">json<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"c\"># A typical reply when you only *ask* for JSON in the prompt<\/span><\/span>\n<span class=\"l\"><span class=\"n\">reply<\/span> = <span class=\"s\">\"\"\"Sure! Here is the data you asked for:<\/span><\/span>\n<span class=\"l\"><span class=\"s\">```json<\/span><\/span>\n<span class=\"l\"><span class=\"s\">{\"name\": \"Asha\", \"age\": \"twenty-six\", \"skills\": [\"Python\", \"SQL\"],}<\/span><\/span>\n<span class=\"l\"><span class=\"s\">```<\/span><\/span>\n<span class=\"l\"><span class=\"s\">Let me know if you need anything else.\"\"\"<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">try<\/span>:<\/span>\n<span class=\"l\">    <span class=\"n\">data<\/span> = <span class=\"n\">json<\/span>.<span class=\"n\">loads<\/span>(<span class=\"n\">reply<\/span>)<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"s\">\"Parsed:\"<\/span>, <span class=\"n\">data<\/span>)<\/span>\n<span class=\"l\"><span class=\"k\">except<\/span> <span class=\"n\">json<\/span>.<span class=\"n\">JSONDecodeError<\/span> <span class=\"k\">as<\/span> <span class=\"n\">err<\/span>:<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"s\">\"JSONDecodeError:\"<\/span>, <span class=\"n\">err<\/span>)<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><\/div><pre><code><span class=\"gp\">$<\/span> python naive_json.py\nJSONDecodeError: Expecting value: line 1 column 1 (char 0)<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 14, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Python<\/span><\/div>\n<\/div>\n<div class=\"dd-vsc-pane p2\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a naive-json.mjs<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\">\/\/ A typical reply when you only *ask* for JSON in the prompt<\/span><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">reply<\/span> = <span class=\"s\">`Sure! Here is the data you asked for:<\/span><\/span>\n<span class=\"l\"><span class=\"s\">\\`\\`\\`json<\/span><\/span>\n<span class=\"l\"><span class=\"s\">{\"name\": \"Asha\", \"age\": \"twenty-six\", \"skills\": [\"Python\", \"SQL\"],}<\/span><\/span>\n<span class=\"l\"><span class=\"s\">\\`\\`\\`<\/span><\/span>\n<span class=\"l\"><span class=\"s\">Let me know if you need anything else.`<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">try<\/span> {<\/span>\n<span class=\"l\">  <span class=\"kc\">const<\/span> <span class=\"n\">data<\/span> = <span class=\"nb\">JSON<\/span>.<span class=\"n\">parse<\/span>(<span class=\"n\">reply<\/span>);<\/span>\n<span class=\"l\">  <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">\"Parsed:\"<\/span>, <span class=\"n\">data<\/span>);<\/span>\n<span class=\"l\">} <span class=\"k\">catch<\/span> (<span class=\"n\">err<\/span>) {<\/span>\n<span class=\"l\">  <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">`${<\/span><span class=\"n\">err<\/span>.<span class=\"n\">name<\/span><span class=\"s\">}: ${<\/span><span class=\"n\">err<\/span>.<span class=\"n\">message<\/span><span class=\"s\">}`<\/span>);<\/span>\n<span class=\"l\">}<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><\/div><pre><code><span class=\"gp\">$<\/span> node naive-json.mjs\nSyntaxError: Unexpected token 'S', \"Sure! Here\"... is not valid JSON<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 13, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>JavaScript<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Click the <strong>PY<\/strong> and <strong>JS<\/strong> tabs to switch files, and <strong>\u25b6 Run<\/strong> to see the real output.<\/p>\n\n<p>Look at everything that went wrong in one small reply: a friendly sentence before the data, Markdown <code>```json<\/code> fences, <code>\"age\"<\/code> written as words instead of a number, a trailing comma and a closing sentence. Your parser fails on the very first character. Even if you strip the extra text with a regular expression, you still get the wrong type for <code>age<\/code> \u2014 and next time the model may forget a key completely.<\/p>\n\n<h2 id=\"three-levels\">Prompt-only vs JSON mode vs structured outputs<\/h2>\n<p>There are three levels of &#8220;make the model return JSON&#8221;. Click each one to compare them:<\/p>\n\n<div class=\"dd-tabs\">\n<input type=\"radio\" name=\"ti\" id=\"ti1\" checked>\n<input type=\"radio\" name=\"ti\" id=\"ti2\">\n<input type=\"radio\" name=\"ti\" id=\"ti3\">\n<div class=\"labels\"><label for=\"ti1\">1 \u00b7 Prompt only<\/label><label for=\"ti2\">2 \u00b7 JSON mode<\/label><label for=\"ti3\">3 \u00b7 Structured outputs<\/label><\/div>\n<div class=\"panel p1\">\n<p><strong>How:<\/strong> you write &#8220;Reply with JSON like {&#8220;name&#8221;: \u2026}&#8221; in the prompt.<\/p>\n<div class=\"dd-levels\"><div><b>Valid JSON?<\/b>Usually<div class=\"dd-meter\"><i style=\"width:60%\"><\/i><\/div><\/div><div><b>Right keys and types?<\/b>Often not<div class=\"dd-meter\"><i style=\"width:40%\"><\/i><\/div><\/div><div><b>Works with<\/b>Every model<\/div><\/div>\n<p>Fine for a quick test. Never trust it in production without validation.<\/p>\n<\/div>\n<div class=\"panel p2\">\n<p><strong>How:<\/strong> set <code>{\"type\": \"json_object\"}<\/code> (OpenAI) or <code>format: \"json\"<\/code> (Ollama).<\/p>\n<div class=\"dd-levels\"><div><b>Valid JSON?<\/b>Yes<div class=\"dd-meter\"><i style=\"width:95%\"><\/i><\/div><\/div><div><b>Right keys and types?<\/b>Not guaranteed<div class=\"dd-meter\"><i style=\"width:55%\"><\/i><\/div><\/div><div><b>Works with<\/b>Most APIs<\/div><\/div>\n<p>The reply always parses, but the model can still choose its own keys and types. You must still mention &#8220;JSON&#8221; in the prompt.<\/p>\n<\/div>\n<div class=\"panel p3\">\n<p><strong>How:<\/strong> send a JSON Schema (from Pydantic or Zod) with <code>strict<\/code> mode.<\/p>\n<div class=\"dd-levels\"><div><b>Valid JSON?<\/b>Yes<div class=\"dd-meter\"><i style=\"width:100%\"><\/i><\/div><\/div><div><b>Right keys and types?<\/b>Yes, by design<div class=\"dd-meter\"><i style=\"width:98%\"><\/i><\/div><\/div><div><b>Works with<\/b>OpenAI, Ollama, Gemini, Claude and more<\/div><\/div>\n<p>The best choice whenever your provider supports it. The values can still be <em>wrong<\/em> (a wrong category, for example), so keep business checks in your code.<\/p>\n<\/div>\n<\/div>\n\n<h2 id=\"define-schema\">Define the shape once with Pydantic and Zod<\/h2>\n<p>Writing JSON Schema by hand is long and easy to get wrong. Instead, describe your data with a normal class or object, and let a library create the schema. In Python we use <a href=\"https:\/\/docs.pydantic.dev\/latest\/\" rel=\"noopener\" target=\"_blank\">Pydantic<\/a>; in JavaScript we use <a href=\"https:\/\/zod.dev\/\" rel=\"noopener\" target=\"_blank\">Zod<\/a>. Our example is a support desk: turn a customer email into a <strong>ticket<\/strong>.<\/p>\n\n<div class=\"dd-vsc dd-vsc-termonly\" role=\"group\" aria-label=\"Terminal: Terminal \u2014 set up the project\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">Terminal \u2014 set up the project<\/span><\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># 1. project folder (Python 3.10+ and Node.js 20.6+)<\/span><\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> mkdir devdojo-json &amp;&amp; cd devdojo-json<\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> pip install openai pydantic ollama python-dotenv<\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> npm init -y &amp;&amp; npm pkg set type=module<\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> npm install openai zod ollama<\/span>\n<span class=\"l\"><span class=\"c\"># 2a. cloud: put your key in a .env file<\/span><\/span>\n<span class=\"l\"><span class=\"c\"># OPENAI_API_KEY=sk-your-openai-key<\/span><\/span>\n<span class=\"l\"><span class=\"c\"># 2b. or free and local: make sure Ollama is running (Part 6)<\/span><\/span>\n<span class=\"l\"><span class=\"gp\">$<\/span> ollama pull llama3.2<\/span><\/code><\/pre>\n<\/div>\n\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: schema.py, schema.mjs\">\n<input type=\"radio\" name=\"v2\" id=\"v2f1\" class=\"f1\" checked>\n<input type=\"radio\" name=\"v2\" id=\"v2f2\" class=\"f2\">\n<input type=\"checkbox\" id=\"v2r1\" class=\"r1\">\n<input type=\"checkbox\" id=\"v2r2\" class=\"r2\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">schema.py \u2014 devdojo-json \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v2f1\" class=\"t1\"><b class=\"ic py\">PY<\/b>schema.py<\/label>\n<label for=\"v2f2\" class=\"t2\"><b class=\"ic js\">JS<\/b>schema.mjs<\/label>\n<label for=\"v2r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<label for=\"v2r2\" class=\"run run2\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a schema.py<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># pip install pydantic<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">typing<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">Literal<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">pydantic<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">BaseModel<\/span>, <span class=\"n\">Field<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">class<\/span> <span class=\"nc\">Ticket<\/span>(<span class=\"n\">BaseModel<\/span>):<\/span>\n<span class=\"l\">    <span class=\"s\">\"\"\"A customer support ticket extracted from an email.\"\"\"<\/span><\/span>\n<span class=\"l\">    <span class=\"n\">customer_name<\/span>: <span class=\"nb\">str<\/span><\/span>\n<span class=\"l\">    <span class=\"n\">category<\/span>: <span class=\"n\">Literal<\/span>[<span class=\"s\">\"billing\"<\/span>, <span class=\"s\">\"bug\"<\/span>, <span class=\"s\">\"feature_request\"<\/span>, <span class=\"s\">\"other\"<\/span>]<\/span>\n<span class=\"l\">    <span class=\"n\">priority<\/span>: <span class=\"nb\">int<\/span> = <span class=\"n\">Field<\/span>(<span class=\"n\">ge<\/span>=<span class=\"m\">1<\/span>, <span class=\"n\">le<\/span>=<span class=\"m\">5<\/span>, <span class=\"n\">description<\/span>=<span class=\"s\">\"1 = low, 5 = urgent\"<\/span>)<\/span>\n<span class=\"l\">    <span class=\"n\">summary<\/span>: <span class=\"nb\">str<\/span> = <span class=\"n\">Field<\/span>(<span class=\"n\">description<\/span>=<span class=\"s\">\"One short sentence\"<\/span>)<\/span>\n<span class=\"l\">    <span class=\"n\">order_id<\/span>: <span class=\"nb\">str<\/span> | <span class=\"kc\">None<\/span>  <span class=\"c\"># nullable, but the key must always be present<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">if<\/span> <span class=\"n\">__name__<\/span> == <span class=\"s\">\"__main__\"<\/span>:<\/span>\n<span class=\"l\">    <span class=\"n\">schema<\/span> = <span class=\"n\">Ticket<\/span>.<span class=\"n\">model_json_schema<\/span>()<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"s\">\"required:\"<\/span>, <span class=\"n\">schema<\/span>[<span class=\"s\">\"required\"<\/span>])<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"s\">\"category:\"<\/span>, <span class=\"n\">schema<\/span>[<span class=\"s\">\"properties\"<\/span>][<span class=\"s\">\"category\"<\/span>][<span class=\"s\">\"enum\"<\/span>])<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"s\">\"priority:\"<\/span>, <span class=\"n\">schema<\/span>[<span class=\"s\">\"properties\"<\/span>][<span class=\"s\">\"priority\"<\/span>])<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><\/div><pre><code><span class=\"gp\">$<\/span> python schema.py\nrequired: ['customer_name', 'category', 'priority', 'summary', 'order_id']\ncategory: ['billing', 'bug', 'feature_request', 'other']\npriority: {'description': '1 = low, 5 = urgent', 'maximum': 5, 'minimum': 1, 'title': 'Priority', 'type': 'integer'}<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 17, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Python<\/span><\/div>\n<\/div>\n<div class=\"dd-vsc-pane p2\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a schema.mjs<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\">\/\/ npm install zod<\/span><\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">z<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\"zod\"<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">export<\/span> <span class=\"kc\">const<\/span> <span class=\"n\">Ticket<\/span> = <span class=\"n\">z<\/span>.<span class=\"n\">object<\/span>({<\/span>\n<span class=\"l\">  <span class=\"n\">customer_name<\/span>: <span class=\"n\">z<\/span>.<span class=\"n\">string<\/span>(),<\/span>\n<span class=\"l\">  <span class=\"n\">category<\/span>: <span class=\"n\">z<\/span>.<span class=\"k\">enum<\/span>([<span class=\"s\">\"billing\"<\/span>, <span class=\"s\">\"bug\"<\/span>, <span class=\"s\">\"feature_request\"<\/span>, <span class=\"s\">\"other\"<\/span>]),<\/span>\n<span class=\"l\">  <span class=\"n\">priority<\/span>: <span class=\"n\">z<\/span>.<span class=\"n\">number<\/span>().<span class=\"k\">int<\/span>().<span class=\"n\">min<\/span>(<span class=\"m\">1<\/span>).<span class=\"n\">max<\/span>(<span class=\"m\">5<\/span>).<span class=\"n\">describe<\/span>(<span class=\"s\">\"1 = low, 5 = urgent\"<\/span>),<\/span>\n<span class=\"l\">  <span class=\"n\">summary<\/span>: <span class=\"n\">z<\/span>.<span class=\"n\">string<\/span>().<span class=\"n\">describe<\/span>(<span class=\"s\">\"One short sentence\"<\/span>),<\/span>\n<span class=\"l\">  <span class=\"n\">order_id<\/span>: <span class=\"n\">z<\/span>.<span class=\"n\">string<\/span>().<span class=\"n\">nullable<\/span>(), <span class=\"c\">\/\/ nullable, but the key must always be present<\/span><\/span>\n<span class=\"l\">});<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">if<\/span> (<span class=\"n\">process<\/span>.<span class=\"n\">argv<\/span>[<span class=\"m\">1<\/span>]?.<span class=\"n\">endsWith<\/span>(<span class=\"s\">\"schema.mjs\"<\/span>)) {<\/span>\n<span class=\"l\">  <span class=\"kc\">const<\/span> <span class=\"n\">schema<\/span> = <span class=\"n\">z<\/span>.<span class=\"n\">toJSONSchema<\/span>(<span class=\"n\">Ticket<\/span>);<\/span>\n<span class=\"l\">  <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">\"required:\"<\/span>, <span class=\"n\">schema<\/span>.<span class=\"n\">required<\/span>);<\/span>\n<span class=\"l\">  <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">\"category:\"<\/span>, <span class=\"n\">schema<\/span>.<span class=\"n\">properties<\/span>.<span class=\"n\">category<\/span>.<span class=\"k\">enum<\/span>);<\/span>\n<span class=\"l\">  <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">\"priority:\"<\/span>, <span class=\"n\">schema<\/span>.<span class=\"n\">properties<\/span>.<span class=\"n\">priority<\/span>);<\/span>\n<span class=\"l\">}<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><\/div><pre><code><span class=\"gp\">$<\/span> node schema.mjs\nrequired: [ 'customer_name', 'category', 'priority', 'summary', 'order_id' ]\ncategory: [ 'billing', 'bug', 'feature_request', 'other' ]\npriority: {\n  type: 'integer',\n  minimum: 1,\n  maximum: 5,\n  description: '1 = low, 5 = urgent'\n}<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 17, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>JavaScript<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Both files print part of the JSON Schema they create. Click <strong>\u25b6 Run<\/strong> on each tab.<\/p>\n\n<p>A few things to notice, because they matter for every schema you write:<\/p>\n<ul>\n<li><strong><code>Literal[...]<\/code> \/ <code>z.enum([...])<\/code><\/strong> limits a field to fixed values. This is perfect for categories, statuses and labels.<\/li>\n<li><strong><code>Field(ge=1, le=5)<\/code> \/ <code>.min(1).max(5)<\/code><\/strong> adds limits, and <strong><code>description<\/code><\/strong> tells the model what the field means. Descriptions are part of your prompt \u2014 write them clearly.<\/li>\n<li><strong><code>order_id<\/code> is nullable but still required.<\/strong> In strict mode every key must be present. When a value may be missing, allow <code>null<\/code> instead of leaving the key out.<\/li>\n<\/ul>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"openai\">Structured outputs with the OpenAI API<\/h2>\n<p>The OpenAI SDKs have a helper called <code>parse<\/code>. You give it your Pydantic model or Zod schema; it sends a strict JSON Schema, waits for the reply and hands you back a ready-to-use object in <code>output_parsed<\/code>. Put a sample email in <code>email.txt<\/code> (third tab) and run the script for your language. See the official <a href=\"https:\/\/platform.openai.com\/docs\/guides\/structured-outputs\" rel=\"noopener\" target=\"_blank\">OpenAI structured outputs guide<\/a> for every option.<\/p>\n\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: extract_openai.py, extract-openai.mjs, email.txt\">\n<input type=\"radio\" name=\"v3\" id=\"v3f1\" class=\"f1\" checked>\n<input type=\"radio\" name=\"v3\" id=\"v3f2\" class=\"f2\">\n<input type=\"radio\" name=\"v3\" id=\"v3f3\" class=\"f3\">\n<input type=\"checkbox\" id=\"v3r1\" class=\"r1\">\n<input type=\"checkbox\" id=\"v3r2\" class=\"r2\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">extract_openai.py \u2014 devdojo-json \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v3f1\" class=\"t1\"><b class=\"ic py\">PY<\/b>extract_openai.py<\/label>\n<label for=\"v3f2\" class=\"t2\"><b class=\"ic js\">JS<\/b>extract-openai.mjs<\/label>\n<label for=\"v3f3\" class=\"t3\"><b class=\"ic txt\">TXT<\/b>email.txt<\/label>\n<label for=\"v3r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<label for=\"v3r2\" class=\"run run2\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a extract_openai.py<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># pip install openai pydantic python-dotenv<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">import<\/span> <span class=\"n\">os<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">dotenv<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">load_dotenv<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">openai<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">OpenAI<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">schema<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">Ticket<\/span>  <span class=\"c\"># the Pydantic model from schema.py<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">load_dotenv<\/span>()<\/span>\n<span class=\"l\"><span class=\"n\">client<\/span> = <span class=\"n\">OpenAI<\/span>()  <span class=\"c\"># reads OPENAI_API_KEY from .env<\/span><\/span>\n<span class=\"l\"><span class=\"n\">email<\/span> = <span class=\"nb\">open<\/span>(<span class=\"s\">\"email.txt\"<\/span>, <span class=\"n\">encoding<\/span>=<span class=\"s\">\"utf-8\"<\/span>).<span class=\"n\">read<\/span>()<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">response<\/span> = <span class=\"n\">client<\/span>.<span class=\"n\">responses<\/span>.<span class=\"n\">parse<\/span>(<\/span>\n<span class=\"l\">    <span class=\"n\">model<\/span>=<span class=\"n\">os<\/span>.<span class=\"n\">getenv<\/span>(<span class=\"s\">\"OPENAI_MODEL\"<\/span>, <span class=\"s\">\"gpt-6-luna\"<\/span>),<\/span>\n<span class=\"l\">    <span class=\"n\">instructions<\/span>=<span class=\"s\">\"Extract a support ticket from the customer email.\"<\/span>,<\/span>\n<span class=\"l\">    <span class=\"nb\">input<\/span>=<span class=\"n\">email<\/span>,<\/span>\n<span class=\"l\">    <span class=\"n\">text_format<\/span>=<span class=\"n\">Ticket<\/span>,  <span class=\"c\"># &lt;- the SDK turns the model into a strict JSON Schema<\/span><\/span>\n<span class=\"l\">)<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">ticket<\/span> = <span class=\"n\">response<\/span>.<span class=\"n\">output_parsed<\/span>  <span class=\"c\"># a real Ticket object, already validated<\/span><\/span>\n<span class=\"l\"><span class=\"k\">if<\/span> <span class=\"n\">ticket<\/span> is <span class=\"kc\">None<\/span>:<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"s\">\"No ticket. Status:\"<\/span>, <span class=\"n\">response<\/span>.<span class=\"n\">status<\/span>)  <span class=\"c\"># refusal or cut off<\/span><\/span>\n<span class=\"l\"><span class=\"k\">else<\/span>:<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"n\">ticket<\/span>.<span class=\"n\">category<\/span>, <span class=\"n\">ticket<\/span>.<span class=\"n\">priority<\/span>, <span class=\"n\">ticket<\/span>.<span class=\"n\">order_id<\/span>)<\/span>\n<span class=\"l\">    <span class=\"nb\">print<\/span>(<span class=\"n\">ticket<\/span>.<span class=\"n\">model_dump_json<\/span>(<span class=\"n\">indent<\/span>=<span class=\"m\">2<\/span>))<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Example output \u2013 the summary wording will differ<\/span><\/div><pre><code><span class=\"gp\">$<\/span> python extract_openai.py\nbilling 5 A-4821\n{\n  \"customer_name\": \"Rahul Mehta\",\n  \"category\": \"billing\",\n  \"priority\": 5,\n  \"summary\": \"Customer was charged twice for order A-4821 and wants a refund today.\",\n  \"order_id\": \"A-4821\"\n}<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 23, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Python<\/span><\/div>\n<\/div>\n<div class=\"dd-vsc-pane p2\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a extract-openai.mjs<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\">\/\/ npm install openai zod<\/span><\/span>\n<span class=\"l\"><span class=\"c\">\/\/ run with: node --env-file=.env extract-openai.mjs<\/span><\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">readFile<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\"node:fs\/promises\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> <span class=\"n\">OpenAI<\/span> <span class=\"k\">from<\/span> <span class=\"s\">\"openai\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">zodTextFormat<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\"openai\/helpers\/zod\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">Ticket<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\".\/schema.mjs\"<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">client<\/span> = new <span class=\"n\">OpenAI<\/span>(); <span class=\"c\">\/\/ reads OPENAI_API_KEY<\/span><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">email<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">readFile<\/span>(<span class=\"s\">\"email.txt\"<\/span>, <span class=\"s\">\"utf8\"<\/span>);<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">response<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">client<\/span>.<span class=\"n\">responses<\/span>.<span class=\"n\">parse<\/span>({<\/span>\n<span class=\"l\">  <span class=\"n\">model<\/span>: <span class=\"n\">process<\/span>.<span class=\"n\">env<\/span>.<span class=\"n\">OPENAI_MODEL<\/span> ?? <span class=\"s\">\"gpt-6-luna\"<\/span>,<\/span>\n<span class=\"l\">  <span class=\"n\">instructions<\/span>: <span class=\"s\">\"Extract a support ticket from the customer email.\"<\/span>,<\/span>\n<span class=\"l\">  <span class=\"n\">input<\/span>: <span class=\"n\">email<\/span>,<\/span>\n<span class=\"l\">  <span class=\"n\">text<\/span>: { <span class=\"n\">format<\/span>: <span class=\"n\">zodTextFormat<\/span>(<span class=\"n\">Ticket<\/span>, <span class=\"s\">\"ticket\"<\/span>) }, <span class=\"c\">\/\/ &lt;- strict JSON Schema<\/span><\/span>\n<span class=\"l\">});<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">ticket<\/span> = <span class=\"n\">response<\/span>.<span class=\"n\">output_parsed<\/span>; <span class=\"c\">\/\/ typed and validated<\/span><\/span>\n<span class=\"l\"><span class=\"k\">if<\/span> (!<span class=\"n\">ticket<\/span>) {<\/span>\n<span class=\"l\">  <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">\"No ticket. Status:\"<\/span>, <span class=\"n\">response<\/span>.<span class=\"n\">status<\/span>); <span class=\"c\">\/\/ refusal or cut off<\/span><\/span>\n<span class=\"l\">} <span class=\"k\">else<\/span> {<\/span>\n<span class=\"l\">  <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"n\">ticket<\/span>.<span class=\"n\">category<\/span>, <span class=\"n\">ticket<\/span>.<span class=\"n\">priority<\/span>, <span class=\"n\">ticket<\/span>.<span class=\"n\">order_id<\/span>);<\/span>\n<span class=\"l\">  <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"nb\">JSON<\/span>.<span class=\"n\">stringify<\/span>(<span class=\"n\">ticket<\/span>, <span class=\"kc\">null<\/span>, <span class=\"m\">2<\/span>));<\/span>\n<span class=\"l\">}<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Example output \u2013 the summary wording will differ<\/span><\/div><pre><code><span class=\"gp\">$<\/span> node --env-file=.env extract-openai.mjs\nbilling 5 A-4821\n{\n  \"customer_name\": \"Rahul Mehta\",\n  \"category\": \"billing\",\n  \"priority\": 5,\n  \"summary\": \"Customer was charged twice for order A-4821 and wants a refund today.\",\n  \"order_id\": \"A-4821\"\n}<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 24, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>JavaScript<\/span><\/div>\n<\/div>\n<div class=\"dd-vsc-pane p3\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a email.txt<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\">Hi team, I'm Rahul Mehta. I was charged twice for order #A-4821 this morning<\/span>\n<span class=\"l\">and I need the extra payment refunded today, please. Thanks!<\/span><\/code><\/pre>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 2, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>Plain Text<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Switch between the Python, Node.js and <strong>email.txt<\/strong> tabs. The output is an example \u2013 your model&#8217;s summary wording may differ.<\/p>\n\n<p>Notice what you did <em>not<\/em> write: no &#8220;reply in JSON&#8221; instruction, no <code>json.loads<\/code>, no string cleaning. The helper did all of it. In Python <code>ticket<\/code> is a real <code>Ticket<\/code> object, so your editor can autocomplete <code>ticket.priority<\/code>. In Node.js the Zod schema gives you full TypeScript types too.<\/p>\n\n<div class=\"dd-note\"><strong>Why check for <code>None<\/code>?<\/strong> Two things can still stop a parsed result: the model may <strong>refuse<\/strong> an unsafe request (the reply then contains a refusal message instead of data), or the answer may be <strong>cut off<\/strong> because it hit <code>max_output_tokens<\/code> (status <code>incomplete<\/code>). Half a JSON object is not valid, so always handle the &#8220;no data&#8221; case.<\/div>\n\n<h2 id=\"ollama\">Structured outputs with Ollama (free and local)<\/h2>\n<p>Ollama supports the same idea: pass a JSON Schema in the <code>format<\/code> field and the local model can only produce JSON that fits. The official <code>ollama<\/code> packages for Python and JavaScript make this short. Here we create the schema with <code>model_json_schema()<\/code> or <code>z.toJSONSchema()<\/code> and then <strong>validate<\/strong> the reply with the same model \u2014 small local models are much more likely to put a silly value in a correct box.<\/p>\n\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: extract_ollama.py, extract-ollama.mjs\">\n<input type=\"radio\" name=\"v4\" id=\"v4f1\" class=\"f1\" checked>\n<input type=\"radio\" name=\"v4\" id=\"v4f2\" class=\"f2\">\n<input type=\"checkbox\" id=\"v4r1\" class=\"r1\">\n<input type=\"checkbox\" id=\"v4r2\" class=\"r2\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">extract_ollama.py \u2014 devdojo-json \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v4f1\" class=\"t1\"><b class=\"ic py\">PY<\/b>extract_ollama.py<\/label>\n<label for=\"v4f2\" class=\"t2\"><b class=\"ic js\">JS<\/b>extract-ollama.mjs<\/label>\n<label for=\"v4r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<label for=\"v4r2\" class=\"run run2\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a extract_ollama.py<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\"># pip install ollama pydantic   (and: ollama pull llama3.2)<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">ollama<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">chat<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">schema<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">Ticket<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">email<\/span> = <span class=\"nb\">open<\/span>(<span class=\"s\">\"email.txt\"<\/span>, <span class=\"n\">encoding<\/span>=<span class=\"s\">\"utf-8\"<\/span>).<span class=\"n\">read<\/span>()<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">response<\/span> = <span class=\"n\">chat<\/span>(<\/span>\n<span class=\"l\">    <span class=\"n\">model<\/span>=<span class=\"s\">\"llama3.2\"<\/span>,<\/span>\n<span class=\"l\">    <span class=\"n\">messages<\/span>=[<\/span>\n<span class=\"l\">        {<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"system\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"s\">\"Extract a support ticket from the customer email. Reply in JSON.\"<\/span>},<\/span>\n<span class=\"l\">        {<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"user\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"n\">email<\/span>},<\/span>\n<span class=\"l\">    ],<\/span>\n<span class=\"l\">    <span class=\"nb\">format<\/span>=<span class=\"n\">Ticket<\/span>.<span class=\"n\">model_json_schema<\/span>(),  <span class=\"c\"># &lt;- Ollama only generates JSON that fits<\/span><\/span>\n<span class=\"l\">    <span class=\"n\">options<\/span>={<span class=\"s\">\"temperature\"<\/span>: <span class=\"m\">0<\/span>},<\/span>\n<span class=\"l\">)<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">ticket<\/span> = <span class=\"n\">Ticket<\/span>.<span class=\"n\">model_validate_json<\/span>(<span class=\"n\">response<\/span>.<span class=\"n\">message<\/span>.<span class=\"n\">content<\/span>)  <span class=\"c\"># still validate!<\/span><\/span>\n<span class=\"l\"><span class=\"nb\">print<\/span>(<span class=\"n\">ticket<\/span>.<span class=\"n\">model_dump_json<\/span>(<span class=\"n\">indent<\/span>=<span class=\"m\">2<\/span>))<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Example output \u2013 a local model may word things differently<\/span><\/div><pre><code><span class=\"gp\">$<\/span> python extract_ollama.py\n{\n  \"customer_name\": \"Rahul Mehta\",\n  \"category\": \"billing\",\n  \"priority\": 5,\n  \"summary\": \"Customer was charged twice for order A-4821 and wants a refund today.\",\n  \"order_id\": \"A-4821\"\n}<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 18, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Python<\/span><\/div>\n<\/div>\n<div class=\"dd-vsc-pane p2\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a extract-ollama.mjs<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"c\">\/\/ npm install ollama zod   (and: ollama pull llama3.2)<\/span><\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">readFile<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\"node:fs\/promises\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> <span class=\"n\">ollama<\/span> <span class=\"k\">from<\/span> <span class=\"s\">\"ollama\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">z<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\"zod\"<\/span>;<\/span>\n<span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">Ticket<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\".\/schema.mjs\"<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">email<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">readFile<\/span>(<span class=\"s\">\"email.txt\"<\/span>, <span class=\"s\">\"utf8\"<\/span>);<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">response<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">ollama<\/span>.<span class=\"n\">chat<\/span>({<\/span>\n<span class=\"l\">  <span class=\"n\">model<\/span>: <span class=\"s\">\"llama3.2\"<\/span>,<\/span>\n<span class=\"l\">  <span class=\"n\">messages<\/span>: [<\/span>\n<span class=\"l\">    { <span class=\"n\">role<\/span>: <span class=\"s\">\"system\"<\/span>, <span class=\"n\">content<\/span>: <span class=\"s\">\"Extract a support ticket from the customer email. Reply in JSON.\"<\/span> },<\/span>\n<span class=\"l\">    { <span class=\"n\">role<\/span>: <span class=\"s\">\"user\"<\/span>, <span class=\"n\">content<\/span>: <span class=\"n\">email<\/span> },<\/span>\n<span class=\"l\">  ],<\/span>\n<span class=\"l\">  <span class=\"n\">format<\/span>: <span class=\"n\">z<\/span>.<span class=\"n\">toJSONSchema<\/span>(<span class=\"n\">Ticket<\/span>), <span class=\"c\">\/\/ &lt;- Ollama only generates JSON that fits<\/span><\/span>\n<span class=\"l\">  <span class=\"n\">options<\/span>: { <span class=\"n\">temperature<\/span>: <span class=\"m\">0<\/span> },<\/span>\n<span class=\"l\">});<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">ticket<\/span> = <span class=\"n\">Ticket<\/span>.<span class=\"n\">parse<\/span>(<span class=\"nb\">JSON<\/span>.<span class=\"n\">parse<\/span>(<span class=\"n\">response<\/span>.<span class=\"n\">message<\/span>.<span class=\"n\">content<\/span>)); <span class=\"c\">\/\/ still validate!<\/span><\/span>\n<span class=\"l\"><span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"nb\">JSON<\/span>.<span class=\"n\">stringify<\/span>(<span class=\"n\">ticket<\/span>, <span class=\"kc\">null<\/span>, <span class=\"m\">2<\/span>));<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><span class=\"dd-term-note\">Example output \u2013 a local model may word things differently<\/span><\/div><pre><code><span class=\"gp\">$<\/span> node extract-ollama.mjs\n{\n  \"customer_name\": \"Rahul Mehta\",\n  \"category\": \"billing\",\n  \"priority\": 5,\n  \"summary\": \"Customer was charged twice for order A-4821 and wants a refund today.\",\n  \"order_id\": \"A-4821\"\n}<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 20, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>JavaScript<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Make sure Ollama is running and you pulled <code>llama3.2<\/code> (see Part 6). Then click <strong>\u25b6 Run<\/strong>.<\/p>\n\n<div class=\"dd-tip\"><strong>Tip:<\/strong> set <code>temperature<\/code> to <code>0<\/code> for extraction tasks. You want the same answer every time, not a creative one (you learned about temperature in <a href=\"https:\/\/devdojo.co.in\/?p=244\">Part 5<\/a>). It also helps to say &#8220;Reply in JSON&#8221; in the system message, so the model&#8217;s normal habits agree with the schema.<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"validate-retry\">Validate and retry: your safety net<\/h2>\n<p>Structured outputs fix the <em>shape<\/em>. They do not stop every problem: some providers or older models only support JSON mode, a schema feature may be ignored, or you might add rules a schema cannot express. The fix is a simple loop: <strong>validate \u2192 if it fails, show the model its errors \u2192 ask again<\/strong>, up to a few times.<\/p>\n\n<figure>\n<svg viewBox=\"0 0 1000 250\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Validate and retry loop: ask the LLM, validate the reply with Pydantic or Zod; if valid, use the data; if invalid, send the error messages back to the LLM and ask again, at most three times\">\n<rect width=\"1000\" height=\"250\" rx=\"14\" fill=\"#0f1430\"\/>\n<defs><marker id=\"ar2\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto\"><path d=\"M0 0L10 5L0 10z\" fill=\"#a99bff\"\/><\/marker><\/defs>\n<g font-family=\"Segoe UI,Arial,sans-serif\" text-anchor=\"middle\">\n<rect x=\"40\" y=\"80\" width=\"180\" height=\"70\" rx=\"12\" fill=\"#1b2250\" stroke=\"#3178c6\" stroke-width=\"2\"\/><text x=\"130\" y=\"112\" font-size=\"18\" font-weight=\"700\" fill=\"#fff\">1. Ask the LLM<\/text><text x=\"130\" y=\"135\" font-size=\"14\" fill=\"#aab4e8\">schema + email<\/text>\n<rect x=\"320\" y=\"80\" width=\"200\" height=\"70\" rx=\"12\" fill=\"#1b2250\" stroke=\"#7c5cff\" stroke-width=\"2\"\/><text x=\"420\" y=\"112\" font-size=\"18\" font-weight=\"700\" fill=\"#fff\">2. Validate<\/text><text x=\"420\" y=\"135\" font-size=\"14\" fill=\"#aab4e8\">Pydantic \/ Zod<\/text>\n<rect x=\"640\" y=\"40\" width=\"300\" height=\"60\" rx=\"12\" fill=\"#16331f\" stroke=\"#89e051\" stroke-width=\"2\"\/><text x=\"790\" y=\"76\" font-size=\"17\" font-weight=\"700\" fill=\"#b8f0a0\">Valid \u2192 use the data<\/text>\n<rect x=\"640\" y=\"140\" width=\"300\" height=\"70\" rx=\"12\" fill=\"#3a1d2a\" stroke=\"#ff7b72\" stroke-width=\"2\"\/><text x=\"790\" y=\"170\" font-size=\"17\" font-weight=\"700\" fill=\"#ffb3ad\">Invalid \u2192 send errors back<\/text><text x=\"790\" y=\"194\" font-size=\"14\" fill=\"#ffd0cc\">max 3 tries, then give up<\/text>\n<path d=\"M220 115 H 316\" stroke=\"#a99bff\" stroke-width=\"3\" marker-end=\"url(#ar2)\"\/>\n<path d=\"M520 105 L 636 72\" stroke=\"#a99bff\" stroke-width=\"3\" marker-end=\"url(#ar2)\"\/>\n<path d=\"M520 125 L 636 172\" stroke=\"#a99bff\" stroke-width=\"3\" marker-end=\"url(#ar2)\"\/>\n<path d=\"M640 200 C 400 240, 160 240, 130 154\" fill=\"none\" stroke=\"#ff7b72\" stroke-width=\"2.5\" stroke-dasharray=\"7 6\" marker-end=\"url(#ar2)\"\/>\n<text x=\"380\" y=\"236\" font-size=\"14\" fill=\"#ffb3ad\">&#8220;category must be billing, bug, \u2026&#8221;<\/text>\n<\/g>\n<\/svg>\n<figcaption>The validate-and-retry loop. Sending the exact error messages back usually fixes the reply on the second try.<\/figcaption>\n<\/figure>\n\n<p>The example below uses a <strong>pretend LLM<\/strong> that answers badly the first time and correctly the second time, so you can run it without any API key and see real validation errors. In your app, replace <code>ask_llm<\/code> \/ <code>askLLM<\/code> with a real call from the sections above.<\/p>\n\n<div class=\"dd-vsc\" role=\"group\" aria-label=\"Code editor: validate_retry.py, validate-retry.mjs\">\n<input type=\"radio\" name=\"v5\" id=\"v5f1\" class=\"f1\" checked>\n<input type=\"radio\" name=\"v5\" id=\"v5f2\" class=\"f2\">\n<input type=\"checkbox\" id=\"v5r1\" class=\"r1\">\n<input type=\"checkbox\" id=\"v5r2\" class=\"r2\">\n<div class=\"dd-vsc-bar\"><span class=\"dd-dots\"><i><\/i><i><\/i><i><\/i><\/span><span class=\"dd-vsc-title\">validate_retry.py \u2014 devdojo-json \u2014 Visual Studio Code<\/span><\/div>\n<div class=\"dd-vsc-tabs\">\n<label for=\"v5f1\" class=\"t1\"><b class=\"ic py\">PY<\/b>validate_retry.py<\/label>\n<label for=\"v5f2\" class=\"t2\"><b class=\"ic js\">JS<\/b>validate-retry.mjs<\/label>\n<label for=\"v5r1\" class=\"run run1\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<label for=\"v5r2\" class=\"run run2\" title=\"Show the output in the terminal\">\u25b6 Run<\/label>\n<\/div>\n<div class=\"dd-vsc-pane p1\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a validate_retry.py<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"kn\">import<\/span> <span class=\"n\">json<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">pydantic<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">ValidationError<\/span><\/span>\n<span class=\"l\"><span class=\"kn\">from<\/span> <span class=\"n\">schema<\/span> <span class=\"kn\">import<\/span> <span class=\"n\">Ticket<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"c\"># Pretend LLM: the first reply is broken, the second one is fixed.<\/span><\/span>\n<span class=\"l\"><span class=\"n\">fake_replies<\/span> = <span class=\"nb\">iter<\/span>([<\/span>\n<span class=\"l\">    <span class=\"s\">'{\"customer_name\": \"Rahul\", \"category\": \"payment\", \"priority\": 9, \"summary\": \"Charged twice\"}'<\/span>,<\/span>\n<span class=\"l\">    <span class=\"s\">'{\"customer_name\": \"Rahul\", \"category\": \"billing\", \"priority\": 5, '<\/span><\/span>\n<span class=\"l\">    <span class=\"s\">'\"summary\": \"Charged twice for one order\", \"order_id\": \"A-4821\"}'<\/span>,<\/span>\n<span class=\"l\">])<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">def<\/span> <span class=\"nf\">ask_llm<\/span>(<span class=\"n\">messages<\/span>):<\/span>\n<span class=\"l\">    <span class=\"k\">return<\/span> <span class=\"nb\">next<\/span>(<span class=\"n\">fake_replies<\/span>)  <span class=\"c\"># swap in a real API call here<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">def<\/span> <span class=\"nf\">get_ticket<\/span>(<span class=\"n\">email<\/span>, <span class=\"n\">max_tries<\/span>=<span class=\"m\">3<\/span>):<\/span>\n<span class=\"l\">    <span class=\"n\">messages<\/span> = [{<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"user\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"s\">f\"Extract a ticket as JSON:\\n{<\/span><span class=\"n\">email<\/span><span class=\"s\">}\"<\/span>}]<\/span>\n<span class=\"l\">    <span class=\"k\">for<\/span> <span class=\"n\">attempt<\/span> in <span class=\"nb\">range<\/span>(<span class=\"m\">1<\/span>, <span class=\"n\">max_tries<\/span> + <span class=\"m\">1<\/span>):<\/span>\n<span class=\"l\">        <span class=\"n\">reply<\/span> = <span class=\"n\">ask_llm<\/span>(<span class=\"n\">messages<\/span>)<\/span>\n<span class=\"l\">        <span class=\"k\">try<\/span>:<\/span>\n<span class=\"l\">            <span class=\"k\">return<\/span> <span class=\"n\">Ticket<\/span>.<span class=\"n\">model_validate_json<\/span>(<span class=\"n\">reply<\/span>)<\/span>\n<span class=\"l\">        <span class=\"k\">except<\/span> <span class=\"n\">ValidationError<\/span> <span class=\"k\">as<\/span> <span class=\"n\">err<\/span>:<\/span>\n<span class=\"l\">            <span class=\"nb\">print<\/span>(<span class=\"s\">f\"Attempt {<\/span><span class=\"n\">attempt<\/span><span class=\"s\">} failed with {<\/span><span class=\"n\">err<\/span>.<span class=\"n\">error_count<\/span>()<span class=\"s\">} errors:\"<\/span>)<\/span>\n<span class=\"l\">            <span class=\"k\">for<\/span> <span class=\"n\">e<\/span> in <span class=\"n\">err<\/span>.<span class=\"n\">errors<\/span>():<\/span>\n<span class=\"l\">                <span class=\"nb\">print<\/span>(<span class=\"s\">\"  -\"<\/span>, <span class=\"s\">\".\"<\/span>.<span class=\"n\">join<\/span>(<span class=\"nb\">map<\/span>(<span class=\"nb\">str<\/span>, <span class=\"n\">e<\/span>[<span class=\"s\">\"loc\"<\/span>])), <span class=\"s\">\"-&gt;\"<\/span>, <span class=\"n\">e<\/span>[<span class=\"s\">\"msg\"<\/span>])<\/span>\n<span class=\"l\">            <span class=\"c\"># show the model its mistakes and ask again<\/span><\/span>\n<span class=\"l\">            <span class=\"n\">messages<\/span> += [<\/span>\n<span class=\"l\">                {<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"assistant\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"n\">reply<\/span>},<\/span>\n<span class=\"l\">                {<span class=\"s\">\"role\"<\/span>: <span class=\"s\">\"user\"<\/span>, <span class=\"s\">\"content\"<\/span>: <span class=\"s\">f\"Fix these errors and reply with JSON only:\\n{<\/span><span class=\"n\">err<\/span><span class=\"s\">}\"<\/span>},<\/span>\n<span class=\"l\">            ]<\/span>\n<span class=\"l\">    <span class=\"k\">raise<\/span> <span class=\"n\">RuntimeError<\/span>(<span class=\"s\">\"No valid JSON after retries\"<\/span>)<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"nb\">print<\/span>(<span class=\"s\">\"OK:\"<\/span>, <span class=\"n\">get_ticket<\/span>(<span class=\"s\">\"I was charged twice for order #A-4821\"<\/span>))<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><\/div><pre><code><span class=\"gp\">$<\/span> python validate_retry.py\nAttempt 1 failed with 3 errors:\n  - category -&gt; Input should be 'billing', 'bug', 'feature_request' or 'other'\n  - priority -&gt; Input should be less than or equal to 5\n  - order_id -&gt; Field required\nOK: customer_name='Rahul' category='billing' priority=5 summary='Charged twice for one order' order_id='A-4821'<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 32, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 4<\/span><span>UTF-8<\/span><span>LF<\/span><span>Python<\/span><\/div>\n<\/div>\n<div class=\"dd-vsc-pane p2\">\n<div class=\"dd-vsc-crumbs\">devdojo-json \u203a validate-retry.mjs<\/div>\n<pre class=\"dd-vsc-code\"><code><span class=\"l\"><span class=\"k\">import<\/span> { <span class=\"n\">Ticket<\/span> } <span class=\"k\">from<\/span> <span class=\"s\">\".\/schema.mjs\"<\/span>;<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"c\">\/\/ Pretend LLM: the first reply is broken, the second one is fixed.<\/span><\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">fakeReplies<\/span> = [<\/span>\n<span class=\"l\">  <span class=\"s\">'{\"customer_name\": \"Rahul\", \"category\": \"payment\", \"priority\": 9, \"summary\": \"Charged twice\"}'<\/span>,<\/span>\n<span class=\"l\">  <span class=\"s\">'{\"customer_name\": \"Rahul\", \"category\": \"billing\", \"priority\": 5, \"summary\": \"Charged twice for one order\", \"order_id\": \"A-4821\"}'<\/span>,<\/span>\n<span class=\"l\">];<\/span>\n<span class=\"l\"><span class=\"kc\">const<\/span> <span class=\"n\">askLLM<\/span> = <span class=\"k\">async<\/span> (<span class=\"n\">messages<\/span>) =&gt; <span class=\"n\">fakeReplies<\/span>.<span class=\"n\">shift<\/span>(); <span class=\"c\">\/\/ swap in a real API call<\/span><\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"k\">async<\/span> <span class=\"kc\">function<\/span> <span class=\"n\">getTicket<\/span>(<span class=\"n\">email<\/span>, <span class=\"n\">maxTries<\/span> = <span class=\"m\">3<\/span>) {<\/span>\n<span class=\"l\">  <span class=\"kc\">const<\/span> <span class=\"n\">messages<\/span> = [{ <span class=\"n\">role<\/span>: <span class=\"s\">\"user\"<\/span>, <span class=\"n\">content<\/span>: <span class=\"s\">`Extract a ticket as JSON:\\n${<\/span><span class=\"n\">email<\/span><span class=\"s\">}`<\/span> }];<\/span>\n<span class=\"l\">  <span class=\"k\">for<\/span> (<span class=\"kc\">let<\/span> <span class=\"n\">attempt<\/span> = <span class=\"m\">1<\/span>; <span class=\"n\">attempt<\/span> &lt;= <span class=\"n\">maxTries<\/span>; <span class=\"n\">attempt<\/span>++) {<\/span>\n<span class=\"l\">    <span class=\"kc\">const<\/span> <span class=\"n\">reply<\/span> = <span class=\"k\">await<\/span> <span class=\"n\">askLLM<\/span>(<span class=\"n\">messages<\/span>);<\/span>\n<span class=\"l\">    <span class=\"kc\">const<\/span> <span class=\"n\">result<\/span> = <span class=\"n\">Ticket<\/span>.<span class=\"n\">safeParse<\/span>(<span class=\"nb\">JSON<\/span>.<span class=\"n\">parse<\/span>(<span class=\"n\">reply<\/span>));<\/span>\n<span class=\"l\">    <span class=\"k\">if<\/span> (<span class=\"n\">result<\/span>.<span class=\"n\">success<\/span>) <span class=\"k\">return<\/span> <span class=\"n\">result<\/span>.<span class=\"n\">data<\/span>;<\/span>\n<span class=\"l\">    <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">`Attempt ${<\/span><span class=\"n\">attempt<\/span><span class=\"s\">} failed with ${<\/span><span class=\"n\">result<\/span>.<span class=\"n\">error<\/span>.<span class=\"n\">issues<\/span>.<span class=\"n\">length<\/span><span class=\"s\">} errors:`<\/span>);<\/span>\n<span class=\"l\">    <span class=\"k\">for<\/span> (<span class=\"kc\">const<\/span> <span class=\"n\">issue<\/span> <span class=\"k\">of<\/span> <span class=\"n\">result<\/span>.<span class=\"n\">error<\/span>.<span class=\"n\">issues<\/span>) {<\/span>\n<span class=\"l\">      <span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">\"  -\"<\/span>, <span class=\"n\">issue<\/span>.<span class=\"n\">path<\/span>.<span class=\"n\">join<\/span>(<span class=\"s\">\".\"<\/span>), <span class=\"s\">\"-&gt;\"<\/span>, <span class=\"n\">issue<\/span>.<span class=\"n\">message<\/span>);<\/span>\n<span class=\"l\">    }<\/span>\n<span class=\"l\">    <span class=\"n\">messages<\/span>.<span class=\"n\">push<\/span>(<\/span>\n<span class=\"l\">      { <span class=\"n\">role<\/span>: <span class=\"s\">\"assistant\"<\/span>, <span class=\"n\">content<\/span>: <span class=\"n\">reply<\/span> },<\/span>\n<span class=\"l\">      { <span class=\"n\">role<\/span>: <span class=\"s\">\"user\"<\/span>, <span class=\"n\">content<\/span>: <span class=\"s\">`Fix these errors and reply with JSON only:\\n${<\/span><span class=\"n\">result<\/span>.<span class=\"n\">error<\/span>.<span class=\"n\">message<\/span><span class=\"s\">}`<\/span> },<\/span>\n<span class=\"l\">    );<\/span>\n<span class=\"l\">  }<\/span>\n<span class=\"l\">  <span class=\"k\">throw<\/span> new <span class=\"n\">Error<\/span>(<span class=\"s\">\"No valid JSON after retries\"<\/span>);<\/span>\n<span class=\"l\">}<\/span>\n<span class=\"l\"><\/span>\n<span class=\"l\"><span class=\"n\">console<\/span>.<span class=\"n\">log<\/span>(<span class=\"s\">\"OK:\"<\/span>, <span class=\"k\">await<\/span> <span class=\"n\">getTicket<\/span>(<span class=\"s\">\"I was charged twice for order #A-4821\"<\/span>));<\/span><\/code><\/pre>\n<div class=\"dd-vsc-term\"><div class=\"dd-term-head\"><span>TERMINAL<\/span><span>OUTPUT<\/span><span>PROBLEMS<\/span><\/div><pre><code><span class=\"gp\">$<\/span> node validate-retry.mjs\nAttempt 1 failed with 3 errors:\n  - category -&gt; Invalid option: expected one of \"billing\"|\"bug\"|\"feature_request\"|\"other\"\n  - priority -&gt; Too big: expected number to be &lt;=5\n  - order_id -&gt; Invalid input: expected string, received undefined\nOK: {\n  customer_name: 'Rahul',\n  category: 'billing',\n  priority: 5,\n  summary: 'Charged twice for one order',\n  order_id: 'A-4821'\n}<\/code><\/pre><\/div>\n<div class=\"dd-vsc-status\"><span>\u2387 main<\/span><span>Ln 28, Col 1<\/span><span class=\"sp\"><\/span><span>Spaces: 2<\/span><span>UTF-8<\/span><span>LF<\/span><span>JavaScript<\/span><\/div>\n<\/div>\n<\/div>\n<p class=\"dd-vsc-hint\">Run both tabs: Pydantic and Zod catch the same three mistakes with their own messages.<\/p>\n\n<p>The error messages are clear enough for a person <em>and<\/em> for a model. That is why feeding them back works so well. Libraries such as <a href=\"https:\/\/python.useinstructor.com\/\" rel=\"noopener\" target=\"_blank\">Instructor<\/a> wrap exactly this loop for you, but now you know what happens inside.<\/p>\n\n<h2 id=\"schema-rules\">Schema rules to remember (strict mode)<\/h2>\n<p>Strict structured outputs support most of JSON Schema, but with a few rules. Breaking them gives an error <em>before<\/em> the model runs, which is good \u2014 you find out early.<\/p>\n<div class=\"dd-cards\">\n<div class=\"dd-card\"><b>Every key is required<\/b>Use <code>str | None<\/code> or <code>.nullable()<\/code> for optional data. Zod <code>.optional()<\/code> alone throws an error, and the Python SDK quietly marks fields with defaults as required.<\/div>\n<div class=\"dd-card\"><b>No extra keys<\/b>Objects get <code>additionalProperties: false<\/code>. The SDK helpers add this for you.<\/div>\n<div class=\"dd-card\"><b>Order matters<\/b>Keys are written in schema order. Put a <code>reasoning<\/code> field <em>before<\/em> the <code>answer<\/code> field if you want the model to think first.<\/div>\n<div class=\"dd-card\"><b>Keep it small<\/b>Deeply nested or huge schemas are slower and have limits. Split big jobs into several smaller calls.<\/div>\n<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"errors\">Common errors and fixes<\/h2>\n<div class=\"dd-tablewrap\"><table>\n<thead><tr><th>Problem<\/th><th>Why it happens<\/th><th>Fix<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>JSONDecodeError<\/code> \/ <code>Unexpected token<\/code><\/td><td>You only asked for JSON in the prompt; the reply has extra text or fences.<\/td><td>Use structured outputs (<code>parse<\/code> or Ollama <code>format<\/code>) instead of parsing free text.<\/td><\/tr>\n<tr><td>Error: <code>.optional()<\/code> without <code>.nullable()<\/code> is not supported<\/td><td>Strict mode needs every key; a Zod field uses <code>.optional()<\/code>.<\/td><td>Make it nullable: <code>str | None<\/code> with no default, or <code>.nullable()<\/code>.<\/td><\/tr>\n<tr><td><code>output_parsed<\/code> is <code>None<\/code><\/td><td>The model refused, or the answer was cut off by <code>max_output_tokens<\/code>.<\/td><td>Check <code>response.status<\/code> and refusals; raise the token limit or shorten the input.<\/td><\/tr>\n<tr><td>JSON mode error: prompt must mention JSON<\/td><td>Plain JSON mode needs the word &#8220;JSON&#8221; in your messages.<\/td><td>Add &#8220;Reply in JSON&#8221; to the instructions, or switch to a schema.<\/td><\/tr>\n<tr><td>Valid JSON, wrong values (e.g. wrong category)<\/td><td>The schema controls the shape, not the meaning.<\/td><td>Better field descriptions, a few examples in the prompt, <code>temperature 0<\/code>, and business checks in code.<\/td><\/tr>\n<tr><td>Local model ignores the schema<\/td><td>Old Ollama version, or the model is too small for the task.<\/td><td>Update Ollama, try a bigger model, and keep the validate-and-retry loop.<\/td><\/tr>\n<\/tbody>\n<\/table><\/div>\n\n<h2 id=\"best-practices\">Best practices for reliable JSON from LLMs<\/h2>\n<ul>\n<li><strong>One source of truth:<\/strong> define the shape once in Pydantic or Zod and reuse it for the API call, validation and your database or UI types.<\/li>\n<li><strong>Use enums for choices.<\/strong> A category as free text will come back as &#8220;Billing&#8221;, &#8220;billing issue&#8221; and &#8220;payments&#8221;. An enum gives you exactly one spelling.<\/li>\n<li><strong>Describe every field.<\/strong> Short descriptions such as &#8220;ISO date, YYYY-MM-DD&#8221; or &#8220;one short sentence&#8221; improve the values a lot.<\/li>\n<li><strong>Allow &#8220;unknown&#8221;.<\/strong> Give the model a <code>null<\/code> or an <code>\"other\"<\/code> option, so it does not invent data when the email does not contain it.<\/li>\n<li><strong>Validate anyway<\/strong>, especially with local or older models, and log failed replies so you can improve the prompt.<\/li>\n<li><strong>Do not trust the content blindly.<\/strong> The data came from user text. Check ranges, IDs and permissions before you act on it (more on this in the Guardrails part of the series).<\/li>\n<\/ul>\n\n<h2 id=\"practice\">Practice task<\/h2>\n<div class=\"dd-card\">\n<p><strong>Build a resume parser.<\/strong> Create a <code>Candidate<\/code> schema with <code>name<\/code>, <code>email<\/code> (nullable), <code>years_experience<\/code> (integer, 0\u201350), <code>skills<\/code> (list of strings) and <code>level<\/code> (<code>\"junior\" | \"mid\" | \"senior\"<\/code>).<\/p>\n<ol>\n<li>Paste a short resume into <code>resume.txt<\/code> and extract it with OpenAI <strong>or<\/strong> Ollama using the code from this post.<\/li>\n<li>Add the validate-and-retry loop, and test it by temporarily changing the schema so that <code>level<\/code> must be <code>\"lead\"<\/code> \u2014 watch the retry messages.<\/li>\n<li>Bonus: add a <code>reasoning<\/code> field <em>before<\/em> <code>level<\/code> and compare whether the levels become more sensible.<\/li>\n<li>Bonus 2: stream the JSON (Part 7) and show each field on screen as soon as it is complete.<\/li>\n<\/ol>\n<\/div>\n\n<h2 id=\"faq\">Frequently asked questions<\/h2>\n<details><summary>What is the difference between JSON mode and structured outputs?<\/summary><p>JSON mode only promises valid JSON syntax. Structured outputs promise that the JSON also matches your schema: the same keys, types and allowed values every time.<\/p><\/details>\n<details><summary>Do I still need to validate when using structured outputs?<\/summary><p>Yes, it is cheap and safe. The schema guarantees the shape, but refusals, cut-off answers, local models and wrong-but-valid values still happen. Validation turns those into clear errors.<\/p><\/details>\n<details><summary>Is structured output the same as function calling?<\/summary><p>They use the same schema idea. Structured outputs shape the model&#8217;s final answer; function calling (tool use) lets the model ask your code to run a function with arguments that match a schema. We cover tools in Part 14.<\/p><\/details>\n<details><summary>Can I use structured outputs with Claude or Gemini?<\/summary><p>Yes. Most major providers now support JSON Schema based outputs, and the same Pydantic and Zod schemas work. Only the SDK call changes, so check each provider&#8217;s docs for the exact parameter name.<\/p><\/details>\n<details><summary>Does structured output make answers worse?<\/summary><p>For extraction and classification it usually makes them better. For long creative writing, keep the text in a single string field, or let the model write freely and extract the data in a second call.<\/p><\/details>\n<details><summary>Can I stream structured outputs?<\/summary><p>Yes. The SDKs can stream partial JSON while it is being written, so you can show fields as they arrive. Just validate the final, complete object.<\/p><\/details>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>You now know how to get reliable JSON from LLMs. Prompt-only JSON breaks, JSON mode fixes the syntax, and <strong>structured outputs<\/strong> fix the shape by forcing the model to follow a JSON Schema. You defined that schema once with Pydantic and Zod, used it with OpenAI&#8217;s <code>parse<\/code> helper and with a free local model in Ollama, and added a validate-and-retry loop that turns rare failures into clear, fixable errors. This is the bridge between &#8220;an AI that chats&#8221; and &#8220;an AI that powers real features&#8221;.<\/p>\n<p>Missed the previous part? Read <a href=\"https:\/\/devdojo.co.in\/?p=284\">Part 7: Stream LLM Responses and Build a Chat UI<\/a>. Using FastAPI? Our <a href=\"https:\/\/devdojo.co.in\/?p=203\">FastAPI tutorial<\/a> uses the same Pydantic models for request and response bodies.<\/p>\n\n<div class=\"dd-next\"><strong>Next part \u2013 Generative AI Part 9: Embeddings Explained: Turning Text into Vectors.<\/strong> We start the intermediate section. You will learn how text becomes a list of numbers, how to measure meaning with cosine similarity, and build a tiny semantic search \u2014 the first step towards RAG.<\/div>\n\n<div class=\"dd-cta\">\n<h3>New posts every day on DevDojo<\/h3>\n<p>Follow the full <strong>Generative AI series<\/strong> \u2013 30 parts from &#8220;what is an LLM&#8221; to building a production AI assistant. Bookmark <a href=\"https:\/\/devdojo.co.in\/\">devdojo.co.in<\/a> and share the schema you built in the comments.<\/p>\n<\/div>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.<\/p>\n","protected":false},"author":1,"featured_media":302,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[38],"tags":[113,165,44,100,112,164,166],"class_list":["post-303","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","tag-generative-ai","tag-json-schema","tag-llm","tag-pydantic","tag-series-generative-ai","tag-structured-outputs","tag-zod"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.4 - aioseo.com -->\n\t<meta name=\"description\" content=\"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Lokendra\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/\" \/>\n\t\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.4\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"DevDojo - Empowering Developers\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"Structured Outputs: Get Reliable JSON from LLMs | DevDojo\" \/>\n\t\t<meta property=\"og:description\" content=\"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-11-08T14:33:13+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-11-08T14:33:13+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Structured Outputs: Get Reliable JSON from LLMs | DevDojo\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BlogPosting\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#blogposting\",\"name\":\"Structured Outputs: Get Reliable JSON from LLMs | DevDojo\",\"headline\":\"Generative AI Part 8: Structured Outputs for Reliable JSON from LLMs\",\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/genai-series-part-8-structured-outputs-json.jpg\",\"width\":1200,\"height\":630,\"caption\":\"Generative AI series Part 8 banner: messy LLM text passes through a JSON Schema and becomes valid JSON\"},\"datePublished\":\"2026-11-08T14:33:13+00:00\",\"dateModified\":\"2026-11-08T14:33:13+00:00\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#webpage\"},\"articleSection\":\"AI, Generative AI, JSON Schema, LLM, Pydantic, Series: Generative AI, Structured Outputs, Zod\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/ai\\\/#listItem\",\"name\":\"AI\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/ai\\\/#listItem\",\"position\":2,\"name\":\"AI\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/ai\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#listItem\",\"name\":\"Generative AI Part 8: Structured Outputs for Reliable JSON from LLMs\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#listItem\",\"position\":3,\"name\":\"Generative AI Part 8: Structured Outputs for Reliable JSON from LLMs\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/ai\\\/#listItem\",\"name\":\"AI\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/\",\"name\":\"Lokendra\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#authorImage\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g\",\"width\":96,\"height\":96,\"caption\":\"Lokendra\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#webpage\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/\",\"name\":\"Structured Outputs: Get Reliable JSON from LLMs | DevDojo\",\"description\":\"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/genai-series-part-8-structured-outputs-json.jpg\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#mainImage\",\"width\":1200,\"height\":630,\"caption\":\"Generative AI series Part 8 banner: messy LLM text passes through a JSON Schema and becomes valid JSON\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/11\\\/08\\\/genai-series-part-8-structured-outputs-json\\\/#mainImage\"},\"datePublished\":\"2026-11-08T14:33:13+00:00\",\"dateModified\":\"2026-11-08T14:33:13+00:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Structured Outputs: Get Reliable JSON from LLMs | DevDojo","description":"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.","canonical_url":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BlogPosting","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#blogposting","name":"Structured Outputs: Get Reliable JSON from LLMs | DevDojo","headline":"Generative AI Part 8: Structured Outputs for Reliable JSON from LLMs","author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/11\/genai-series-part-8-structured-outputs-json.jpg","width":1200,"height":630,"caption":"Generative AI series Part 8 banner: messy LLM text passes through a JSON Schema and becomes valid JSON"},"datePublished":"2026-11-08T14:33:13+00:00","dateModified":"2026-11-08T14:33:13+00:00","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#webpage"},"isPartOf":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#webpage"},"articleSection":"AI, Generative AI, JSON Schema, LLM, Pydantic, Series: Generative AI, Structured Outputs, Zod"},{"@type":"BreadcrumbList","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","position":1,"name":"Home","item":"https:\/\/devdojo.co.in\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/#listItem","name":"AI"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/#listItem","position":2,"name":"AI","item":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#listItem","name":"Generative AI Part 8: Structured Outputs for Reliable JSON from LLMs"},"previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#listItem","position":3,"name":"Generative AI Part 8: Structured Outputs for Reliable JSON from LLMs","previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/#listItem","name":"AI"}}]},{"@type":"Organization","@id":"https:\/\/devdojo.co.in\/#organization","name":"DevDojo","description":"Empowering Developers","url":"https:\/\/devdojo.co.in\/"},{"@type":"Person","@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author","url":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/","name":"Lokendra","image":{"@type":"ImageObject","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g","width":96,"height":96,"caption":"Lokendra"}},{"@type":"WebPage","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#webpage","url":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/","name":"Structured Outputs: Get Reliable JSON from LLMs | DevDojo","description":"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/devdojo.co.in\/#website"},"breadcrumb":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#breadcrumblist"},"author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"creator":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/11\/genai-series-part-8-structured-outputs-json.jpg","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#mainImage","width":1200,"height":630,"caption":"Generative AI series Part 8 banner: messy LLM text passes through a JSON Schema and becomes valid JSON"},"primaryImageOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/#mainImage"},"datePublished":"2026-11-08T14:33:13+00:00","dateModified":"2026-11-08T14:33:13+00:00"},{"@type":"WebSite","@id":"https:\/\/devdojo.co.in\/#website","url":"https:\/\/devdojo.co.in\/","name":"DevDojo","description":"Empowering Developers","inLanguage":"en-US","publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"}}]},"og:locale":"en_US","og:site_name":"DevDojo - Empowering Developers","og:type":"article","og:title":"Structured Outputs: Get Reliable JSON from LLMs | DevDojo","og:description":"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.","og:url":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/","article:published_time":"2026-11-08T14:33:13+00:00","article:modified_time":"2026-11-08T14:33:13+00:00","twitter:card":"summary_large_image","twitter:title":"Structured Outputs: Get Reliable JSON from LLMs | DevDojo","twitter:description":"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js."},"aioseo_meta_data":{"post_id":"303","title":"Structured Outputs: Get Reliable JSON from LLMs | DevDojo","description":"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.","keywords":null,"keyphrases":{"focus":{"keyphrase":"structured outputs","score":82},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":"Structured Outputs: Get Reliable JSON from LLMs | DevDojo","og_description":"Learn structured outputs for LLMs: get reliable JSON from OpenAI and Ollama using Pydantic and Zod schemas, plus validation and retry in Python and Node.js.","og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"BlogPosting","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":0,"frequency":"default","local_seo":null,"breadcrumb_settings":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-11-08 14:32:03","updated":"2026-11-08 15:45:47","focus_keyword":"structured outputs","additional_keywords":null,"truseo_locale":null,"seo_analyzer_scan_date":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/index.php\/category\/ai\/\" title=\"AI\">AI<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tGenerative AI Part 8: Structured Outputs for Reliable JSON from LLMs\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/devdojo.co.in\/"},{"label":"AI","link":"https:\/\/devdojo.co.in\/index.php\/category\/ai\/"},{"label":"Generative AI Part 8: Structured Outputs for Reliable JSON from LLMs","link":"https:\/\/devdojo.co.in\/index.php\/2026\/11\/08\/genai-series-part-8-structured-outputs-json\/"}],"_links":{"self":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/303","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/comments?post=303"}],"version-history":[{"count":1,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/303\/revisions"}],"predecessor-version":[{"id":304,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/303\/revisions\/304"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media\/302"}],"wp:attachment":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media?parent=303"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/categories?post=303"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/tags?post=303"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}