OpenAI Structured Outputs sind ein API-Feature, das das Modell zwingt, JSON zu erzeugen, das exakt einem von dir vorgegebenen JSON Schema entspricht. Nicht per Nachbesserung, sondern durch Constrained Decoding auf Token-Ebene. In Python bindest du Structured Outputs am schnellsten über eine Pydantic-Klasse und den client.responses.parse()-Helper des OpenAI-SDKs. In diesem Praxis-Guide 2026 zeige ich dir, wie du Structured Outputs mit Pydantic einrichtest, Strict Mode ausreizt, Refusals sauber behandelst und dein Schema vor dem Deploy evaluierst.
Structured Outputs erzwingen Schema-Konformität mathematisch. Das Modell kann kein Token generieren, das dein JSON Schema verletzt.
In Python nutzt du am besten client.responses.parse() mit einer Pydantic-BaseModel als text_format; das SDK übernimmt Schema-Generierung, Strict-Flag und Parsing.
JSON Mode (response_format={"type": "json_object"}) gilt 2026 als Legacy. Er garantiert nur gültiges JSON, nicht Schema-Treue.
Structured Outputs setzen mindestens gpt-4o-2024-08-06 voraus; für neue Projekte empfehle ich die GPT-5-Familie und ein explizit gepinntes Model-Snapshot.
Die häufigste Falle ist refusal: Das Modell darf aus Sicherheitsgründen ablehnen und liefert dann kein Schema-JSON. Prüf das Feld vor dem Parsing.
Semantische Regeln (z. B. „Enddatum nach Startdatum") gehören weiterhin in Pydantic-Validatoren, nicht ins JSON Schema.
Was sind OpenAI Structured Outputs?
Structured Outputs (offiziell eingeführt Mitte 2024, seitdem stark ausgebaut) lösen das Problem, dass LLMs syntaktisch fast immer korrektes JSON liefern, aber eben nicht immer. Eine Extraktions-Pipeline, die in 99 % der Fälle sauberes JSON bekommt und in 1 % der Fälle abbricht, verursacht in Produktion Kaskaden-Fehler, die schwer zu reproduzieren sind. Ich hab in einem Kundenprojekt Anfang 2024 zwei Tage damit verbracht, ein einziges „unerwartetes Komma" in einer 40k-Requests-Batch zu debuggen. Genau der Schmerz, den Structured Outputs beseitigen.
Das mentale Modell ist einfach: Beim JSON Mode versucht das Modell, JSON zu erzeugen, und du prüfst selbst nach. Bei Structured Outputs kann der Decoder auf Sampling-Ebene kein Token wählen, das dein Schema verletzt. Die Garantie greift also nicht als Post-Hoc-Check, sondern bereits während der Generierung. Das Modell wird von einer aus deinem JSON Schema kompilierten Zustandsmaschine „geführt" und kann formal keinen falschen Pfad einschlagen.
Wichtige Verwandte in derselben Feature-Familie: die Antwort-Formatierung über response_format={"type": "json_schema"} (Chat Completions API und Responses API), die Tool-Definition mit strict: true beim Function Calling, sowie die native Pydantic-Anbindung im OpenAI-Python-SDK. Alle drei nutzen intern denselben Grammatik-basierten Decoder-Zwang.
Structured Outputs vs. JSON Mode vs. Function Calling
2026 sehe ich in Reviews immer noch Code, der veralteten JSON Mode nutzt, weil ältere Blog-Posts kopiert wurden. Die Unterschiede sind aber wesentlich. Hier eine Übersicht, die ich meinen Teams beim Refactor an die Hand gebe:
Kriterium
JSON Mode (Legacy)
response_format: json_schema
Function Calling strict: true
Garantie
Nur gültiges JSON-Syntax
Schema-konform (Constrained Decoding)
Schema-konforme Tool-Argumente
Einsatzzweck
Freie JSON-Antworten (deprecated)
Direkte strukturierte Antwort an den Nutzer
Externe Tools/APIs aufrufen
Modell-Support
Ab gpt-3.5-turbo-1106
Ab gpt-4o-2024-08-06, GPT-5-Familie
Ab gpt-4-0613, alle neueren
Kombinierbar mit Tools
Ja
Nein, schließt Tool-Nutzung im selben Call aus
Ja, ist die Tool-Nutzung selbst
Refusal-Objekt
Nein
Ja (message.refusal)
Ja, mit Textantwort statt Tool-Call
Empfehlung 2026
Nicht mehr verwenden
Standard für Extraktion & Klassifikation
Standard für Agenten & Tool-Loops
Die Faustregel: Wenn das Modell dem Nutzer strukturiert antwortet, nimm response_format. Wenn das Modell eine externe Funktion aufruft, nimm Function Calling mit strict: true. Beides gemeinsam in einem Call ist nicht möglich, denn response_format und tools schließen sich gegenseitig aus. Ein Agenten-Loop löst das, indem du separate Calls für Tool-Auswahl und Endantwort machst.
Setup: OpenAI-SDK und Pydantic in Python
Für alle Beispiele nutze ich die aktuellen Bibliotheks-Versionen (Stand Juli 2026): das offizielle openai-Paket ab 1.60 und Pydantic 2.10+. Ältere Kombinationen unterstützen zwar auch Structured Outputs, aber die .parse()-Convenience-Methode auf der Responses API kam erst später dazu.
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI() # liest OPENAI_API_KEY aus der Umgebung
Erstes Beispiel: Strukturierte Datenextraktion mit Pydantic
Die praxisrelevanteste Anwendung von Structured Outputs ist Datenextraktion aus Freitext: Rechnungen, E-Mails, Support-Tickets, Chat-Verläufe. Hier ist ein vollständiges, lauffähiges Beispiel, das aus einer Kunden-E-Mail Metadaten extrahiert:
from typing import Literal
from datetime import date
from pydantic import BaseModel, Field, field_validator
from openai import OpenAI
client = OpenAI()
class SupportTicket(BaseModel):
"""Struktur eines aus E-Mail extrahierten Tickets."""
customer_email: str
subject: str
category: Literal["billing", "technical", "account", "other"]
priority: Literal["low", "medium", "high", "urgent"]
requested_action: str
due_date: date | None = Field(
default=None,
description="Fällig-Datum, falls im Text erwähnt (ISO 8601)."
)
@field_validator("customer_email")
@classmethod
def _lowercase_email(cls, v: str) -> str:
return v.strip().lower()
EMAIL = """
Von: [email protected]
Betreff: Rechnung R-2026-0428 zu hoch
Hallo Support-Team, unsere letzte Rechnung enthaelt eine Position ueber 480 EUR,
die wir nicht bestellt haben. Bitte klaeren bis Ende naechster Woche (10.07.2026).
Ohne Rueckmeldung storniere ich per Lastschriftrueckgabe.
Viele Gruesse, Anna Schulz
"""
response = client.responses.parse(
model="gpt-5.5-2026-05-14",
input=[
{"role": "system", "content": "Extrahiere die Ticket-Struktur aus der E-Mail. Datum bitte im Format YYYY-MM-DD."},
{"role": "user", "content": EMAIL},
],
text_format=SupportTicket,
)
ticket = response.output_parsed
if ticket is None:
print("Modell hat abgelehnt:", response.output[0].content[0].refusal)
else:
print(ticket.model_dump_json(indent=2))
Was hier passiert: Das SDK leitet aus SupportTicket ein JSON Schema ab, setzt intern strict: true, sendet den Request und deserialisiert die Antwort direkt in eine Pydantic-Instanz. response.output_parsed ist entweder das gefüllte Modell oder None (bei Refusal). Der Feld-Validator _lowercase_email läuft auf Client-Seite und erzwingt eine semantische Regel, die JSON Schema selbst nicht ausdrücken kann.
Wie funktioniert Strict Mode intern?
Constrained Decoding klingt magisch, ist aber technisch geradlinig. OpenAI kompiliert dein JSON Schema in eine kontextfreie Grammatik (CFG) beziehungsweise einen endlichen Automaten (FSM). Bei jedem Sampling-Schritt maskiert der Decoder die Logits aller Tokens, die den aktuellen Pfad durch die Grammatik verlassen würden. Sie bekommen effektiv Wahrscheinlichkeit null. Übrig bleiben nur Tokens, die mit dem Schema kompatibel sind.
Praktisch heißt das: Selbst wenn das Modell „gerne" ein zusätzliches Feld erfinden würde, kann das Token, das eine neue Property einleiten würde, nicht gesampelt werden. Für dich bedeutet das: kein try/except json.JSONDecodeError, kein Regex-Fixup, keine Retry-Schleife bei Malformed Output. Diese Klasse von Fehlern verschwindet komplett. Das ist der eigentliche Grund, warum ich in neuen Projekten kein einziges Skript ohne Strict Mode mehr sehen will.
Was nicht garantiert wird: die Korrektheit der Werte selbst. Das Modell kann immer noch das falsche Datum, den falschen Betrag oder eine halluzinierte Rechnungsnummer eintragen. JSON Schema erzwingt Struktur, Semantik bleibt Sache deiner Evals und deiner Pydantic-Validatoren. Wer das verwechselt, kauft sich falsche Sicherheit.
Refusals und Fehlermodi richtig behandeln
Der neue Fehlerpfad, der jeden zweiten „Warum kracht meine Pipeline?"-Bug erklärt: Das Modell darf jederzeit ablehnen, wenn es einen Prompt für unsicher hält. Es liefert dann kein Schema-JSON, sondern ein Refusal-Objekt mit natürlicher Sprache. Wer response.output_parsed blind weiterreicht, sieht dann irgendwo tief in der Pipeline eine ValidationError. Ursache: das Modell hat freundlich „nein" gesagt.
from openai import OpenAI
client = OpenAI()
response = client.responses.parse(
model="gpt-5.5-2026-05-14",
input=[...],
text_format=SupportTicket,
)
# Refusal ist eine Modell-Entscheidung, kein transienter Fehler.
first_content = response.output[0].content[0]
if getattr(first_content, "refusal", None):
log.warning("Model refused: %s", first_content.refusal)
return None # nicht retryen, das aendert nichts
ticket = response.output_parsed
Behandle Refusals wie einen HTTP 403, nicht wie einen 503. Retries helfen nicht. Bei sensiblen Domains (Medizin, Recht, Finanzen) empfehle ich ein zusätzliches Feld direkt im Schema:
from typing import Literal
from pydantic import BaseModel
class Extraction(BaseModel):
status: Literal["ok", "refused_internal"]
refusal_reason: str | None = None
data: SupportTicket | None = None
So kannst du im Modell zwischen „ich kann das nicht" (semantische Ablehnung, im Schema kodiert) und einem echten API-Refusal (auf message.refusal) sauber unterscheiden. Der zweite Fall braucht Menschen-Eskalation, der erste ist Business-Logik.
Zweiter häufiger Fehlerpfad: Truncation. Wenn max_output_tokens zu niedrig gesetzt ist, kann die Generierung mitten im JSON abbrechen. Der Response-Status ist dann incomplete. Prüf explizit response.status und logge diesen Fall separat.
Function Calling mit strict: true, wann verwenden?
Structured Outputs über response_format lösen das „Modell antwortet strukturiert an den Nutzer"-Problem. Function Calling mit strict: true löst das „Modell ruft ein Tool auf"-Problem. In einem Agenten-Setup brauchst du in der Regel beides, nur eben in getrennten Calls. Meine Kollegen aus dem Backend fragen mich regelmäßig, warum ich in Reviews auf getrennte Calls dränge; die Antwort ist immer dieselbe: response_format und tools sind mutually exclusive im selben Request.
tools = [{
"type": "function",
"name": "search_orders",
"description": "Suche Bestellungen eines Kunden im letzten Zeitraum.",
"parameters": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"since": {"type": "string", "format": "date"},
"status": {"type": "string", "enum": ["open", "paid", "cancelled"]}
},
"required": ["customer_id", "since", "status"],
"additionalProperties": False
},
"strict": True,
}]
response = client.responses.create(
model="gpt-5.5-2026-05-14",
input=[
{"role": "system", "content": "Nutze bei Bedarf Tools."},
{"role": "user", "content": "Was sind die letzten Bestellungen von Kunde C-1042?"}
],
tools=tools,
parallel_tool_calls=False, # WICHTIG: strict + parallel = inkompatibel
)
Für Agenten-Loops mit Tools empfehle ich Frameworks, die diese Zustände ordentlich modellieren. Der Vergleich zwischen LangGraph, CrewAI und AutoGen zeigt, welches Framework für welchen Loop-Typ passt.
Schema-Design, Limits und Latenz-Kosten
Strict Mode ist gratis, dein Schema ist es nicht. Jedes Feld, jede Union, jedes Enum, jede Verschachtelungsebene kostet dich Eingabe-Tokens und potenziell Latenz, weil der Decoder mehr Möglichkeiten prüfen muss. Aus meiner Praxis diese Leitplanken:
Höchstens ca. 30 Felder pro Schema. Bei mehr splittest du besser in mehrere Calls oder eine Hierarchie aus Optional-Referenzen.
Verschachtelungstiefe max. 5. OpenAI erlaubt aktuell bis zu 5 Ebenen bei Strict Mode; darüber kommt ein API-Fehler.
Alle Felder als required. Optionalität modellierst du über Optional[X] bzw. X | None in Pydantic. Das SDK übersetzt das in eine Union mit null. Ein required: false von Hand wird in Strict Mode zurückgewiesen.
additionalProperties: false ist Pflicht. Das SDK setzt es automatisch, wenn du Pydantic-Klassen verwendest.
Schema wird jedes Mal mitgesendet. Bei häufig aufgerufenen Endpunkten kombiniere ich Structured Outputs mit Prompt Caching (siehe Anthropic Prompt Caching in der Praxis; das Prinzip ist bei OpenAI vergleichbar, mit automatischem Cache ab 1024 Tokens).
Der erste Aufruf mit einem neuen Schema kann spürbar langsamer sein, weil OpenAI die Grammatik einmal kompilieren muss. Nachfolgende Aufrufe mit demselben Schema sind schnell, weil die kompilierte FSM gecacht ist. Bei häufig geänderten Schemas (z. B. während einer Feedback-Session mit Fachabteilung) spürst du das deutlich.
Evals: Structured Outputs vor dem Deploy testen
Meine unpopuläre Meinung, die ich seit drei Jahren wiederhole: Ohne Evals kein Deploy. Structured Outputs beseitigen eine Fehlerklasse (Syntax), nicht die andere (Semantik). Der Modell-Output ist strukturell korrekt, aber ist er auch inhaltlich richtig? Das siehst du nur mit einer Suite, die reale Beispiele durchtestet.
Ein minimales Eval-Setup mit Pytest, das ich in fast jedem Projekt ausrolle:
Für größere Datensätze und statistische Metriken (Field-Level Accuracy, Confusion Matrices für Enum-Felder) nutze ich Frameworks wie DeepEval oder Promptfoo. Zusätzlich schalte ich in Produktion Tracing dazu. Wie das mit Langfuse aussieht, habe ich in der Deep-Dive-Anleitung zu LLM-Observability mit Langfuse ausführlich gezeigt.
Konkrete Signale, auf die ich immer alarmiere: Anstieg der Refusal-Rate über 1 %, Field-Level Accuracy-Drop bei einem Modell-Upgrade, plötzlich verlängerte P95-Latenz nach Schema-Änderung. Alle drei fangen Probleme ab, die ohne Tracing wochenlang unentdeckt bleiben.
Was ist der Unterschied zwischen JSON Mode und Structured Outputs?
JSON Mode (type: "json_object") garantiert nur, dass die Antwort syntaktisch gültiges JSON ist, nicht, dass sie deinem Schema entspricht. Structured Outputs (type: "json_schema" mit strict: true) garantieren beides: gültiges JSON und Schema-Treue, weil das Modell auf Token-Ebene keine schema-verletzenden Zeichen erzeugen kann. Für neue Projekte solltest du 2026 ausschließlich Structured Outputs verwenden.
Welche OpenAI-Modelle unterstützen Structured Outputs?
Structured Outputs über response_format erfordern mindestens gpt-4o-2024-08-06 oder gpt-4o-mini-2024-07-18. Alle Modelle der GPT-5-Familie (inkl. gpt-5.5 und gpt-5.5-pro) unterstützen sie ebenfalls und sind meine 2026-Standardempfehlung. Function Calling mit strict: true läuft breiter, ab gpt-4-0613 und gpt-3.5-turbo-0613.
Wie behandle ich Refusals bei Structured Outputs?
Prüfe immer message.refusal beziehungsweise content[0].refusalvor dem Parsing. Ein Refusal ist eine bewusste Modell-Entscheidung, kein transienter Fehler. Retries helfen nicht. Behandle ihn wie einen HTTP 403 und logge ihn separat, damit du Refusal-Raten monitoren kannst.
Kann ich Structured Outputs mit Function Calling kombinieren?
Nicht im selben Request. response_format und tools schließen sich gegenseitig aus. In Agenten-Loops löst du das mit getrennten Calls: erst ein Turn für Tool-Nutzung (Function Calling mit strict: true), dann ein Turn für die strukturierte Endantwort (response_format: json_schema). Zusätzlich musst du parallel_tool_calls=False setzen, wenn du Strict Mode nutzt.
Muss ich noch mit Pydantic validieren, wenn Structured Outputs schon garantieren?
Ja, für semantische Regeln, die JSON Schema nicht ausdrücken kann. JSON Schema erzwingt Typen, Enums und Pflichtfelder. Regeln wie „Enddatum liegt nach Startdatum", „Betrag muss positiv sein, wenn Typ = credit" oder Cross-Feld-Konsistenz gehören in Pydantic-@field_validator- oder @model_validator-Methoden. Structured Outputs garantieren Struktur, nicht Bedeutung.
Wie viele Felder darf mein JSON Schema haben?
OpenAI erlaubt aktuell bis zu 100 Properties und 5 Verschachtelungsebenen. In der Praxis empfehle ich, unter 30 Feldern pro Call zu bleiben. Größere Schemas erhöhen Latenz, Token-Kosten und die Wahrscheinlichkeit unsauberer Extraktionen. Splitte lieber in mehrere spezialisierte Calls auf.
Anthropic Prompt Caching reduziert die Claude-API-Kosten um bis zu 90 %. Praxis-Guide für 2026 mit Breakpoint-Strategie, TTL-Wahl, Python-Beispiel und Kostenrechnung.
LangGraph, CrewAI oder AutoGen v0.4? Ehrlicher Vergleich mit Code, Latenz- und Token-Benchmarks plus Entscheidungsmatrix für produktive Multi-Agenten-Systeme 2026.