n4nAI

Structured outputs in the OpenAI API: a code walkthrough

Hands-on step-by-step tutorial for building reliable JSON with the OpenAI API using structured outputs, with runnable code examples and output.

n4n Team3 min read605 words

Audio narration

Coming soon — every post will get a voice note here.

Getting JSON out of a language model used to mean prompting for braces and then writing a parser to recover from missing fields. This openai structured outputs code example walks through the native structured outputs feature of the OpenAI API, which enforces a JSON schema at the API boundary so the response deserializes cleanly every time.

Prerequisites

  • Python 3.10 or newer
  • openai Python package >= 1.40.0 (pip install openai)
  • pydantic >= 2.0 (pip install pydantic) for local validation
  • An OpenAI API key exported as OPENAI_API_KEY
  • A model that supports structured outputs (e.g. gpt-4o-2024-08-06 or gpt-4o-mini-2024-07-18)

Version matters. The json_schema response format type and strict mode shipped in the mid-2024 API refresh. Older SDKs silently ignore response_format or reject the schema. If you are behind a corporate proxy or using a gateway, the request shape is identical as long as the endpoint is OpenAI-compatible.

Define the target schema

Write the schema first, not the prompt. Suppose you ingest receipts and need to pull out vendor, date, line items, and totals.

{
  "type": "object",
  "properties": {
    "vendor": { "type": "string" },
    "date": { "type": "string", "format": "date" },
    "line_items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "description": { "type": "string" },
          "quantity": { "type": "integer" },
          "unit_price": { "type": "number" }
        },
        "required": ["description", "quantity", "unit_price"],
        "additionalProperties": false
      }
    },
    "total": { "type": "number" }
  },
  "required": ["vendor", "date", "line_items", "total"],
  "additionalProperties": false
}

Strict mode requires additionalProperties: false on every object and all keys listed in required. That is not optional; the API rejects the schema otherwise. The format: date hint is documentation, not enforcement—validate it locally later.

First call: basic structured output

Set up the client and send a receipt as a user message.

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

schema = {
    "type": "object",
    "properties": {
        "vendor": {"type": "string"},
        "date": {"type": "string", "format": "date"},
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "description": {"type": "string"},
                    "quantity": {"type": "integer"},
                    "unit_price": {"type": "number"}
                },
                "required": ["description", "quantity", "unit_price"],
                "additionalProperties": False
            }
        },
        "total": {"type": "number"}
    },
    "required": ["vendor", "date", "line_items", "total"],
    "additionalProperties": False
}

resp = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "Extract the receipt data."},
        {"role": "user", "content": "Coffee Bean, 2024-05-12. 2x Espresso at 3.50, 1x Muffin at 2.00. Total 9.00"}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "receipt",
            "strict": True,
            "schema": schema
        }
    }
)

print(resp.choices[0].message.content)

Expected output (formatted for readability):

{
  "vendor": "Coffee Bean",
  "date": "2024-05-12",
  "line_items": [
    {"description": "Espresso", "quantity": 2, "unit_price": 3.50},
    {"description": "Muffin", "quantity": 1, "unit_price": 2.00}
  ],
  "total": 9.00
}

The model cannot return a bare string or a dict missing total. If it tries, the API returns an error instead of malformed JSON. The openai structured outputs code example above is the minimal viable call.

Validate locally with Pydantic

The API guarantees shape, but not semantic correctness (e.g. total could be arithmetically wrong). Validate with a typed model and tighten the date.

from pydantic import BaseModel, field_validator
from typing import List
from datetime import date

class LineItem(BaseModel):
    quantity: int
    unit_price: float

class Receipt(BaseModel):
    vendor: str
    date: str
    line_items: List[LineItem]
    total: float

    @field_validator("date")
    def iso_date(cls, v):
        return date.fromisoformat(v).isoformat()

import json

raw = resp.choices[0].message.content
parsed = Receipt.model_validate(json.loads(raw))
print(parsed.vendor, parsed.total)

Output:

Coffee Bean 9.0

If the model returns 05/12/2024, the validator raises ValueError. Catch it and route to a correction prompt or dead-letter queue.

Handling provider degradation

Structured outputs are only useful if the request succeeds. When you call OpenAI directly, a 429 or 500 leaves you to retry. If you put an OpenAI-compatible endpoint such as n4n.ai in front of the model, the same response_format payload works unchanged and the gateway can apply automatic fallback when a provider is rate-limited or degraded, while still metering per token. That is the only change needed: point base_url at the gateway.

client = OpenAI(
    api_key=os.environ["GATEWAY_KEY"],
    base_url="https://api.n4n.ai/v1"
)
# rest of the call is identical

Common schema mistakes

  • Forgetting additionalProperties: false on nested objects. Strict mode rejects the whole schema.
  • Using nullable or anyOf in ways the subset does not allow. Supported keywords are limited; avoid pattern if you can.
  • Defining deeply recursive types. The API caps depth and breadth; keep schemas flat.
  • Setting strict: true but omitting a field from required. The two are coupled.
  • Assuming format does type coercion. It does not.

Streaming structured output

As of the gpt-4o series, structured outputs work with streaming. You get delta chunks that concatenate to valid JSON. Do not parse each chunk; accumulate then validate.

stream = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "Extract receipt data."},
        {"role": "user", "content": "Coffee Bean, 2024-05-12. 2x Espresso at 3.50, 1x Muffin at 2.00. Total 9.00"}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {"name": "receipt", "strict": True, "schema": schema}
    },
    stream=True
)

parts = []
for chunk in stream:
    if chunk.choices[0].delta.content:
        parts.append(chunk.choices[0].delta.content)
full = "".join(parts)
Receipt.model_validate(json.loads(full))

Streaming does not change the schema contract. The final string is still strictly validated by the API before the stream closes.

Error handling

Wrap the call so transient failures do not crash the pipeline.

from openai import APIError

try:
    resp = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": "Coffee Bean 2024-05-12 total 9"}],
        response_format={
            "type": "json_schema",
            "json_schema": {"name": "receipt", "strict": True, "schema": schema}
        }
    )
except APIError as e:
    print(f"status {e.status_code}: {e.message}")
    # retry or fallback

A 400 with invalid_schema means your JSON Schema violates the strict subset. A 429 means rate limit; back off.

Full runnable script

import os, json
from openai import OpenAI
from pydantic import BaseModel, field_validator
from typing import List
from datetime import date

class LineItem(BaseModel):
    quantity: int
    unit_price: float

class Receipt(BaseModel):
    vendor: str
    date: str
    line_items: List[LineItem]
    total: float

    @field_validator("date")
    def iso_date(cls, v):
        return date.fromisoformat(v).isoformat()

schema = {
    "type": "object",
    "properties": {
        "vendor": {"type": "string"},
        "date": {"type": "string", "format": "date"},
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "description": {"type": "string"},
                    "quantity": {"type": "integer"},
                    "unit_price": {"type": "number"}
                },
                "required": ["description", "quantity", "unit_price"],
                "additionalProperties": False
            }
        },
        "total": {"type": "number"}
    },
    "required": ["vendor", "date", "line_items", "total"],
    "additionalProperties": False
}

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

resp = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "Extract receipt data."},
        {"role": "user", "content": "Coffee Bean, 2024-05-12. 2x Espresso at 3.50, 1x Muffin at 2.00. Total 9.00"}
    ],
    response_format={"type": "json_schema", "json_schema": {"name": "receipt", "strict": True, "schema": schema}}
)

receipt = Receipt.model_validate(json.loads(resp.choices[0].message.content))
print(f"{receipt.vendor} charged {receipt.total}")

Run it. You should see Coffee Bean charged 9.0.

When not to use structured outputs

If your schema changes per request based on arbitrary user input, generating JSON Schema dynamically is fine but keep it small. For free-form summarization, JSON mode adds latency with no benefit. Use structured outputs when the downstream code is rigid and a missing key breaks the pipeline. The openai structured outputs code example shown here is production-shaped: schema first, strict mode on, local validation second, gateway fallback if you need resilience.

Tagsstructured-outputsopenaitutorialcode

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All structured outputs & json mode posts →