{"id":73,"date":"2026-09-16T23:09:39","date_gmt":"2026-09-16T15:09:39","guid":{"rendered":"https:\/\/wp.qoraapi.com\/ai-structured-outputs-json-mode\/"},"modified":"2026-09-20T03:53:16","modified_gmt":"2026-09-19T19:53:16","slug":"ai-structured-outputs-json-mode","status":"publish","type":"post","link":"https:\/\/qoraapi.com\/blog\/ai-structured-outputs-json-mode\/","title":{"rendered":"AI Structured Outputs Explained: JSON Mode, Schema Enforcement, Reliable Parsing"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Structured outputs<\/strong> are how you stop guessing whether the model will return valid JSON. Instead of writing a paragraph asking for &#8220;JSON only&#8221; and hoping for the best, you give the model a JSON Schema that constrains every response, and the API rejects completions that would not parse. The result is the same reliability you get from a typed function call \u2014 your downstream code can trust the shape, your pipelines do not break on stray prose, and your error handling gets simpler.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide explains what structured outputs and JSON mode actually do, how the three major providers implement them differently, how to design schemas that are both expressive enough to be useful and strict enough to be enforced, and the failure modes (refusals, max_tokens truncation, refusals-as-content) that catch teams when they first switch from &#8220;free-form prompts asking for JSON&#8221; to enforced schemas. The pattern pairs naturally with <a href=\"https:\/\/qoraapi.com\/blog\/ai-function-calling-tool-use\/\">function calling<\/a> and benefits from the same <a href=\"https:\/\/qoraapi.com\/blog\/openai-compatible-api-guide\/\">OpenAI-compatible contract<\/a> if you want to swap providers without rewriting your code.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"why-structured-outputs\">Why structured outputs matter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most production AI features pass model output into something else: a database, a UI, a downstream function, an analytics pipeline. The moment the output becomes data rather than text, its shape matters \u2014 a missing field or a typo in a key can break every consumer downstream. &#8220;Just ask for JSON&#8221; is the standard solution, but it is fragile: the model can still return <code>\"Sure, here is the JSON: {...}\"<\/code>, wrap the object in an array, escape characters incorrectly, or halluc additional fields. Code that consumes that output has to be defensive in ways that turn simple tasks into messy parsers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Structured outputs fix this at the source. You supply a JSON Schema; the provider&#8217;s API guarantees the response matches it. Your code becomes a typed function call: <code>resp.parsed<\/code> is a Python object, not a string to wrangle. Reliability goes up, complexity goes down, and the difference is felt most at the edges \u2014 when the prompt is ambiguous, when the model is small, when you switch versions and behaviour shifts.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"json-mode-vs-structured-outputs\">JSON mode vs structured outputs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">These two terms sound similar but they guarantee different things:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>JSON mode<\/strong> ensures the output is valid JSON. The model still chooses the shape \u2014 you cannot pin down which keys appear or what their types are. Useful when you want a JSON object but the schema is trivial or up to the model.<\/li>\n\n<li><strong>Structured outputs<\/strong> enforce a specific JSON Schema. The provider rejects any completion that does not match the schema&#8217;s structure, required fields, and enum constraints. Use this whenever downstream code assumes a particular shape.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">JSON mode is the older, weaker guarantee. Structured outputs are what you want in production. Most modern providers now ship some form of structured outputs, but the level of strictness varies \u2014 and that variation is where most integration bugs live.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"defining-schemas\">Defining a usable schema<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The schema you write is both a contract and a constraint. A good one describes the data you need, nothing more \u2014 and avoids features the provider cannot enforce. Three rules consistently produce schemas that work:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li><strong>Require every field.<\/strong> Marking a property as <code>required<\/code> is the only way to guarantee it appears. Optional fields should have an explicit <code>null<\/code> default in the type list (e.g. <code>\"type\": [\"string\", \"null\"]<\/code>) and be clearly marked.<\/li>\n\n<li><strong>Use enums for closed sets.<\/strong> When a field can only take a few values, declare them with <code>enum<\/code>. This is the single biggest quality improvement available, because the model no longer has to invent plausible-sounding strings.<\/li>\n\n<li><strong>Keep descriptions short and descriptive.<\/strong> The description is what the model reads to decide what value to produce. &#8220;The customer&#8217;s sentiment in one word&#8221; is more useful than a paragraph.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"openai-example\">A complete OpenAI structured-outputs example<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OpenAI&#8217;s structured outputs use a <code>response_format<\/code> with <code>type: \"json_schema\"<\/code> and a JSON Schema that is constrained to a subset the API can enforce. The <code>strict: true<\/code> flag is what turns the schema into a hard guarantee:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from openai import OpenAI\nfrom pydantic import BaseModel\n\nclient = OpenAI()\n\nclass Sentiment(BaseModel):\n    label: str          # \"positive\" | \"neutral\" | \"negative\"\n    score: float        # 0.0 .. 1.0\n    summary: str        # one sentence\n\nSCHEMA = {\n    \"type\": \"json_schema\",\n    \"json_schema\": {\n        \"name\": \"sentiment\",\n        \"strict\": True,\n        \"schema\": Sentiment.model_json_schema(),\n    },\n}\n\nresp = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"system\", \"content\": \"Classify the sentiment of the review.\"},\n        {\"role\": \"user\",   \"content\": \"I waited three weeks and it never arrived.\"},\n    ],\n    response_format=SCHEMA,\n)\n\nresult = Sentiment.model_validate_json(resp.choices[0].message.content)\nprint(result.label, result.score, result.summary)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two details matter. First, <code>strict: true<\/code> rejects any completion that would not parse \u2014 you either get a fully-formed object or an error, never a half-formed one. Second, the first call with a new schema incurs a small one-time cost as the provider compiles the grammar; subsequent calls are fast.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cross-provider\">Cross-provider differences<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you call more than one provider, normalise the schema shape and let a small adapter translate it. The patterns look similar; the field names do not:<\/p>\n\n\n\n<figure class=\"wp-block-table is-style-stripes\"><table><thead><tr><th>Concept<\/th><th>OpenAI<\/th><th>Anthropic Claude<\/th><th>Google Gemini<\/th><\/tr><\/thead><tbody><tr><td>JSON mode<\/td><td><code>response_format: {\"type\": \"json_object\"}<\/code><\/td><td>No native mode; prompt + JSON syntax in output<\/td><td><code>generation_config.response_mime_type = \"application\/json\"<\/code><\/td><\/tr><tr><td>Structured outputs<\/td><td><code>response_format.type = \"json_schema\"<\/code> with <code>strict: true<\/code><\/td><td>Tools (function calling) acts as a strict schema<\/td><td><code>generation_config.response_schema<\/code><\/td><\/tr><tr><td>Schema language<\/td><td>JSON Schema (subset)<\/td><td>JSON Schema (tool input)<\/td><td>OpenAPI 3 subset<\/td><\/tr><tr><td>Refusal handling<\/td><td><code>message.refusal<\/code> field<\/td><td>Stop reason; no special field<\/td><td>Finish reason <code>SAFETY<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The most useful abstraction in a multi-provider stack is a &#8220;schema-bearing request&#8221;&#8221; that compiles to each provider&#8217;s specific shape. Our guide to <a href=\"https:\/\/qoraapi.com\/blog\/openai-compatible-api-guide\/\">the OpenAI-compatible API<\/a> explains why this is exactly the kind of variation a gateway is designed to absorb.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"when-to-use-what\">Structured outputs vs function calling<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Both technologies constrain a model&#8217;s output, but they solve different problems. Choose based on intent:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Use structured outputs<\/strong> when the model should <em>answer<\/em> with a structured value \u2014 classification, extraction, scoring, summarisation into a record. The model&#8217;s output is the data.<\/li>\n\n<li><strong>Use function calling<\/strong> when the model should <em>act<\/em> by requesting that your code run something \u2014 fetching a record, calling another API, querying a database. The model&#8217;s output is a request; your code does the real work.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">It is common to combine both: a function-calling tool whose arguments are themselves validated against a JSON Schema, plus the final assistant response wrapped in structured outputs for safe parsing. The two compose cleanly because both share the same underlying contract.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"failure-modes\">Failure modes that catch teams<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Even with strict schemas, three failure modes recur in production:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Refusals.<\/strong> When the model refuses to answer (safety filter, content policy, ambiguous prompt), the provider returns a refusal object rather than a completion that matches the schema. Code that only checks for a parsed object will mis-handle it. Always inspect the refusal field before validating the body.<\/li>\n\n<li><strong><code>max_tokens<\/code> truncation.<\/strong> If a completion runs out of tokens mid-object, you get a syntactically broken string. Set <code>max_tokens<\/code> generously, validate before storing, and treat &#8220;unparseable output&#8221; as a retry signal \u2014 sometimes a larger budget alone solves it.<\/li>\n\n<li><strong>Unsupported schema features.<\/strong> Each provider enforces a subset of JSON Schema. OpenAPI-style formats (<code>\"format\": \"date-time\"<\/code>), recursive references, and arbitrary unions are commonly restricted. When the API rejects a schema, the error is usually specific enough to point at the feature \u2014 read it carefully and simplify rather than fighting the provider.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"best-practices\">Best practices for production<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Three habits keep structured outputs reliable in the long run:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>Define schemas in code, not prompts.<\/strong> Use Pydantic, Zod, or similar to derive the JSON Schema from a typed class. The model of &#8220;write JSON in the prompt and pray&#8221; is what structured outputs replaces \u2014 keep the prompt focused on intent and let the schema enforce the shape.<\/li>\n\n<li><strong>Validate at the boundary, trust inside.<\/strong> Once the response has parsed, treat it as typed data. Do not defensively re-validate every field in business logic; that adds noise and loses the value of the type.<\/li>\n\n<li><strong>Version your schemas.<\/strong> When the contract changes, old prompts and old responses may not match the new shape. Either ship the new schema as a separate endpoint, or version the response wrapper and migrate callers together.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"checklist\">Structured-outputs checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Derive the schema from a typed class (Pydantic, Zod) rather than hand-writing JSON.<\/li>\n\n<li>Mark every field as <code>required<\/code> unless you genuinely want it optional.<\/li>\n\n<li>Use <code>enum<\/code> for closed sets and short descriptions for everything else.<\/li>\n\n<li>Always check the refusal field before validating the body.<\/li>\n\n<li>Set <code>max_tokens<\/code> generously enough for the longest expected output.<\/li>\n\n<li>Treat unparseable output as a retry signal \u2014 same input, larger budget if needed.<\/li>\n\n<li>Stay within the provider&#8217;s supported schema subset; simplify on rejection.<\/li>\n\n<li>Version the schema and migrate callers together when the shape changes.<\/li>\n\n<li>If you call multiple providers, normalise via an adapter, not by hand in each call site.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faq-json-mode-vs-structured\">What is the difference between JSON mode and structured outputs?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">JSON mode guarantees the response is valid JSON. Structured outputs add a JSON Schema guarantee: the keys, types, enums, and required fields are enforced. In production, structured outputs are almost always what you want \u2014 JSON mode alone is a weak guarantee.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faq-strict\">Does the model always respect the schema?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">With <code>strict: true<\/code> (or the equivalent flag in each provider), the API rejects completions that would not match the schema before they reach your code. You either get a fully-formed object or an error \u2014 never a half-formed one. This is the practical difference from prompting for JSON in plain text.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faq-which-provider\">Which providers support structured outputs?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OpenAI has the strictest implementation under <code>response_format.type = \"json_schema\"<\/code> with <code>strict: true<\/code>. Google Gemini supports an OpenAPI subset via <code>response_schema<\/code>. Anthropic Claude does not expose a native JSON-mode flag, but tool-calling arguments are validated against a JSON Schema and act as a strict contract. If you call more than one, write a small adapter that compiles your schema into each provider&#8217;s format \u2014 see our <a href=\"https:\/\/qoraapi.com\/blog\/openai-compatible-api-guide\/\">OpenAI-compatible API guide<\/a> for the wrapper tradeoffs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faq-refusal\">What happens when the model refuses to answer?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">It does not produce a partial object \u2014 it produces a refusal object alongside (or instead of) the structured content. Code that calls <code>model_validate_json<\/code> on a refusal body will fail. Always check the refusal field first, then parse. Treating refusals as a separate outcome rather than an exception keeps your error handling clean.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faq-fallback\">What if my schema is too complex for the provider?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each provider enforces a subset of JSON Schema. Recursive references, arbitrary unions, and certain <code>format<\/code> keywords are commonly restricted. When the API rejects a schema, the error usually names the unsupported feature. Simplify the schema, or split a complex value into two calls \u2014 a strict schema for the parts you can enforce and a free-form <code>string<\/code> field for the parts you cannot.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faq-cost\">Do structured outputs cost more than regular completions?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Most providers do not charge more for the schema itself, but you still pay for the input and output tokens it produces. The first call with a new schema can incur a small one-time cost as the provider compiles a grammar. Subsequent calls are essentially the same cost as a regular completion. See our <a href=\"https:\/\/qoraapi.com\/blog\/reduce-ai-api-costs\/\">guide to reducing AI API costs<\/a> for related cost patterns.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"faq-vs-function\">Should I use structured outputs or function calling?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use structured outputs when the model should <em>answer<\/em> with data \u2014 classification, extraction, scoring, summarisation. Use function calling when the model should <em>act<\/em> by requesting that your code do something. The two compose: a tool&#8217;s arguments are themselves validated against a JSON Schema, and the final assistant response can also be structured. See our <a href=\"https:\/\/qoraapi.com\/blog\/ai-function-calling-tool-use\/\">guide to function calling<\/a> for the action side of this pattern.<\/p>\n\n\n\n<hr class=\"wp-block-separator\" \/>\n\n\n\n<p class=\"wp-block-paragraph\">Structured outputs are the production-grade answer to &#8220;give me JSON&#8221;. With a typed schema, the API guarantees the shape; with a refusal field, your error handling stays clean; with versioned schemas, your contracts evolve without surprises. The investment is small \u2014 most teams retrofit an existing prompt in under an hour \u2014 and the payoff is felt everywhere the model&#8217;s output becomes data. If you want to try it against multiple models without rewriting your client, create a key at <a href=\"https:\/\/qoraapi.com\/\" target=\"_blank\" rel=\"noopener\">qoraapi.com<\/a> and your code can speak OpenAI&#8217;s <code>response_format<\/code> against GPT, Claude, or Gemini.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Related reading<\/h3>\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/qoraapi.com\/blog\/ai-function-calling-tool-use\/\">AI Function Calling Explained: Tools, JSON Schema, and the Tool-Use Loop<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/document-data-extraction\/\">Extracting Structured Data from Documents with AI APIs<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/text-to-sql-ai\/\">Text-to-SQL: Letting Users Query Your Database with AI<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/detect-reduce-hallucinations\/\">Detecting and Reducing Hallucinations in Production LLM Apps<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/voice-ai-apis\/\">Building Voice AI Apps: TTS, STT, and Realtime APIs<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/vector-database-selection\/\">How to Choose a Vector Database for RAG<\/a><\/li><li><a href=\"https:\/\/qoraapi.com\/blog\/context-window-management\/\">Managing the Context Window: Truncation, Summarization, and Sliding Windows<\/a><\/li><\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>A practical guide to structured outputs and JSON mode in AI APIs: JSON Schema contracts, strict guarantees, OpenAI response_format, Claude and Gemini equivalents, and the failure modes that catch teams in production.<\/p>\n","protected":false},"author":1,"featured_media":72,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[5,6,9,7,11],"class_list":["post-73","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-api","tag-ai-api","tag-api-gateway","tag-developer-tools","tag-developers","tag-software-development"],"_links":{"self":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/73","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/comments?post=73"}],"version-history":[{"count":3,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/73\/revisions"}],"predecessor-version":[{"id":256,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/posts\/73\/revisions\/256"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/media\/72"}],"wp:attachment":[{"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/media?parent=73"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/categories?post=73"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qoraapi.com\/blog\/wp-json\/wp\/v2\/tags?post=73"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}