Wenn Sie ein Modell um eine Antwort in JSON bitten, funktioniert json.loads() meistens problemlos. Die Ausfälle sind die, die Sie eine Debugging-Sitzung kosten: ein einzeiliger Kommentar innerhalb des Objekts, ein Markdown-Code-Fence darum, ein Schlüssel als userEmail geschrieben, wo Ihr Code user_email erwartet. Ein strikterer Prompt (“antworte NUR mit dem JSON-Objekt, kein weiterer Text”) macht diese Fälle seltener. Unmöglich macht er sie nicht, denn das Modell kann die Anweisung trotzdem ignorieren. Strukturierte Ausgabe macht diese Anweisung überflüssig: Sie übergeben der API ein JSON Schema, und das schränkt das Token-Sampling des Modells selbst so ein, dass die Antwort dazu passen muss.

JSON-Mode vs. strukturierte Ausgabe: Was jedes garantiert

Der “JSON-Mode” (response_format: {"type": "json_object"} in der älteren OpenAI-API, und das Äquivalent bei den meisten anderen Anbietern) garantiert eine Sache: Die Ausgabe lässt sich als JSON parsen. Er sagt nichts darüber, welche Schlüssel existieren, welchen Typ sie haben, oder ob ein Feld, auf das Sie sich verlassen, überhaupt vorhanden ist. Ein Modell im JSON-Mode kann {"result": "yes"} zurückgeben, wenn Ihr Code {"status": "approved", "confidence": 0.91} erwartet, und nichts zeigt Ihnen an, dass etwas schiefgelaufen ist.

Strukturierte Ausgabe (OpenAI nennt es Structured Outputs, Anthropic nennt es JSON outputs) ist eine andere Garantie: Die Antwort entspricht dem JSON Schema, das Sie angeben, mit allen erforderlichen Schlüsseln und dem korrekten Typ für jeden. Function Calling löst ein verwandtes Problem: Das Modell bittet Ihren Code, etwas auszuführen. Die beiden werden leicht verwechselt, weil beide ein Schema übergeben. Was sich unterscheidet, ist, was das Schema formt:

FunktionWas sie garantiertWofür sie gedacht ist
JSON-Mode (json_object)Nur gültige JSON-SyntaxLegacy-Fallback, kein Schema durchzusetzen
Strukturierte Ausgabe (strict Schema)JSON entspricht genau Ihrem SchemaAntwort für den Nutzer oder Ihren Code formen
Function Calling / Tool UseTool-Name und Argumente entsprechen dessen SchemaDas Modell entscheidet, Ihren Code aufzurufen

Eine Produktions-Pipeline nutzt oft die letzten beiden zusammen. Strict Tool Use verhindert, dass die Funktionsaufrufe eines Agenten fehlerhaft ankommen, und ein strict Schema auf der finalen Antwort typisiert die Zusammenfassung, die an den Nutzer zurückgeht.

Wie Constrained Decoding die Übereinstimmung erzwingt

Das Modell vergleicht seine Antwort nie nach der Generierung mit Ihrem Schema. Das Schema ändert, was es generieren darf — Token für Token.

Die Serving-Schicht kompiliert Ihr JSON Schema in eine Grammatik: eine Zustandsmaschine, die an jeder Position der Ausgabe verfolgt, welche Token noch zu etwas Gültigem führen würden. Vor jedem Sampling-Schritt setzt die Serving-Schicht den Logit jedes Vokabular-Tokens, das die Grammatik an dieser Position ausschließt, auf minus unendlich — nach dem Softmax hat es dann eine Wahrscheinlichkeit von null. Das Modell sampelt aus dem, was übrig bleibt, und die relativen Wahrscheinlichkeiten unter den verbleibenden Token bleiben erhalten. Ein falsch geschriebener Schlüssel, eine schließende geschweifte Klammer am falschen Ort, ein String, wo das Schema eine Zahl verlangt: Sobald die Grammatik sie ausschließt, sind diese Token keine Option mehr.

Dieser Kompilierungsschritt hat seinen Preis. Eine Grammatik für ein neues Schema zu bauen, braucht echte Arbeit, daher ist die erste Anfrage mit diesem Schema langsamer als die folgenden. Die API von Anthropic cacht die kompilierte Grammatik 24 Stunden nach der letzten Nutzung, und spätere Aufrufe mit demselben Schema überspringen die Kompilierung.

Der Mechanismus erklärt auch die Grenze. Constrained Decoding legt nur die Form fest, sonst nichts. Die Grammatik weiß, welche Token syntaktisch zulässig sind, nicht, welcher davon wahr ist — das Modell kann also weiterhin ein gültiges "email"-Feld mit einer erfundenen Adresse füllen.

So erhalten Sie schema-valides JSON von OpenAI, Claude und Ollama

Die Responses API von OpenAI nimmt das Schema in text.format, mit aktiviertem strict:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-6-astra",
    input="Extract the ticket details: 'Login fails with a 500 after the "
          "password reset flow, seen first on build 4.12, blocking release.'",
    text={
        "format": {
            "type": "json_schema",
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "summary": {"type": "string"},
                    "build": {"type": "string"},
                    "severity": {"type": "string", "enum": ["low", "medium", "high", "blocking"]},
                },
                "required": ["summary", "build", "severity"],
                "additionalProperties": False,
            },
        }
    },
)
print(response.output_text)

Claude nimmt das Schema in output_config.format, nicht in einer Tool-Definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
curl https://api.anthropic.com/v1/messages \
  -H "content-type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -d '{
    "model": "claude-sonnet-5-5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Extract the ticket details: ..."}],
    "output_config": {
      "format": {
        "type": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "summary": {"type": "string"},
            "build": {"type": "string"},
            "severity": {"type": "string", "enum": ["low", "medium", "high", "blocking"]}
          },
          "required": ["summary", "build", "severity"],
          "additionalProperties": false
        }
      }
    }
  }'

Das JSON kommt im Textinhalt der Antwort zurück, nicht in einem Tool-Call-Block. Der alte Workaround (ein Fake-Tool definieren, das Modell zum Aufruf zwingen, die Argumente auslesen) funktioniert weiterhin bei Modellen, die nur Tool Use unterstützen. Wo native strukturierte Ausgabe existiert, brauchen Sie ihn nicht: Der Decoder erzwingt das Schema direkt, statt zu hoffen, dass die Tool-Argumente zufällig passen.

Ein Modell, das lokal mit Ollama läuft, erhält das Schema über das Feld format von /api/chat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
curl http://localhost:11434/api/chat -d '{
  "model": "llama3.2",
  "messages": [{"role": "user", "content": "Extract the ticket details: ..."}],
  "format": {
    "type": "object",
    "properties": {
      "summary": {"type": "string"},
      "build": {"type": "string"},
      "severity": {"type": "string", "enum": ["low", "medium", "high", "blocking"]}
    },
    "required": ["summary", "build", "severity"]
  },
  "stream": false
}'

Der Mechanismus ist derselbe. Die llama.cpp-Runtime von Ollama baut aus dem Schema eine Grammatik und maskiert Token genauso wie die gehosteten APIs.

Welche JSON-Schema-Funktionen unterstützen die Anbieter?

Jeder Anbieter setzt nur eine Teilmenge von JSON Schema durch, und diese Teilmengen unterscheiden sich genug, dass ein Schema, das Sie von einer API in eine andere kopieren, dort nicht mehr funktioniert.

OpenAI verlangt, dass jedes Objekt "additionalProperties": false trägt und jede seiner Eigenschaften in required aufgeführt wird — es gibt keine Möglichkeit, ein Feld als wirklich optional zu markieren, modellieren Sie es also als required und nullable. Anthropic ist großzügiger: Eine Eigenschaft, die nicht in required steht, ist wirklich optional, und required-Eigenschaften erscheinen in der Ausgabe zuerst, gefolgt von den vorhandenen optionalen. Anthropic unterstützt außerdem lokale $ref/$defs, anyOf und const, lehnt aber rekursive Schemas, numerische Grenzen wie minimum und maximum sowie Zeichenketten-Längenbeschränkungen wie maxLength ab. Die Beispiele von OpenAI enthalten ein rekursives $ref: "#" für baumförmige Ausgaben, was die Dokumentation von Anthropic ausdrücklich ausschließt.

Wenn Ihr Schema eine Einschränkung braucht, die der Anbieter nicht unterstützt, verschieben Sie sie in die description des Feldes (“muss eine positive ganze Zahl unter 100 sein”) und prüfen Sie sie selbst nach dem Parsen. Die SDK-Schema-Helfer beider Plattformen tun das automatisch, wenn Sie das Schema aus einem Pydantic- oder Zod-Modell bauen, statt rohes JSON Schema von Hand zu schreiben.

So validieren Sie strukturierte Ausgabe nach dem Parsen

Eine gültige Form ist keine korrekte Antwort. Das Schema garantiert, dass severity einer Ihrer vier Enum-Werte ist. Ob “blocking” die richtige Einstufung für dieses Ticket ist, ist eine andere Frage. Behandeln Sie das geparste Objekt so, wie Sie die Eingabe eines Formulars behandeln würden: Input zum Validieren, keine gesicherte Wahrheit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pydantic import BaseModel, field_validator

class Ticket(BaseModel):
    summary: str
    build: str
    severity: str

    @field_validator("build")
    @classmethod
    def must_look_like_a_build_number(cls, v: str) -> str:
        if not v.replace(".", "").isdigit():
            raise ValueError(f"'{v}' doesn't look like a build number")
        return v

Instructor umschließt OpenAI, Anthropic und die meisten anderen Anbieter mit einer Validierungs- und Retry-Schleife. Sie geben ihm ein Pydantic-Modell. Bei einem Validierungsfehler gibt es den Fehler an das Modell zurück und fragt erneut, bis zu einem Retry-Limit, das Sie festlegen, statt beim ersten Fehlschlag einen ValidationError auszulösen. Es ist auch der praktische Weg zu einer Schema-Garantie bei einem Anbieter oder einem älteren Modell ohne nativen strict Mode: Die Retry-Schleife ersetzt das Enforcement auf Decoder-Ebene, und jeder Fehlschlag kostet einen zusätzlichen Roundtrip.

Feld-Validatoren erkennen fehlerhafte Werte in einer einzelnen Antwort. Um zu prüfen, ob die Antworten über Hunderte von Eingaben hinweg korrekt sind, und ob eine Prompt-Änderung sie verschlechtert hat, brauchen Sie eine Eval Suite.

Wenn strukturierte Ausgabe trotzdem fehlschlägt

Der strict Mode deckt diese Fälle nicht ab. Prüfen Sie sie, bevor Sie einen Bug in Ihrem eigenen Code suchen:

  • Refusals. Ein Modell kann die Antwort verweigern, und der Refusal-Text entspricht nicht Ihrem Schema. Prüfen Sie stop_reason (oder das Äquivalent) vor dem Parsen, nicht erst, wenn der Parser einen Fehler auslöst.
  • Trunkierung. Wird max_tokens mitten in einem Objekt erreicht, entsteht unvollständiges JSON. Die Grammatik kontrolliert, welches Token als Nächstes kommt, nicht, wie viel Platz noch zum Abschließen bleibt. Erhöhen Sie das Limit oder verkürzen Sie das Schema.
  • Nicht erzwungene Pfade. JSON-Mode, eine Retry-Bibliothek auf einem Modell ohne strict Mode, oder ein Anbieter, der ein Schema-Keyword, das er nicht unterstützt, stillschweigend ignoriert, können alle "Blocking" zurückgeben, wenn Ihr Enum "blocking" erwartet. Wenn eine Anfrage in Ihrem System einen dieser Pfade nimmt, normalisieren Sie die Werte beim Eingang.
  • Schemagröße. Ein großes Schema mit vielen anyOf-Zweigen fügt jeder Anfrage zusätzliche Token hinzu, zusätzlich zur Kompilierungsverzögerung für ein neues Schema. Halten Sie Schemas so flach und klein, wie es die Aufgabe zulässt.

Die Garantie des Anbieters ist real, aber Ihr Parsing-Code sollte nicht davon ausgehen, dass sie gehalten hat.

JSON-Mode, strict Schema oder Function Calling: Was passt wann

Der einfache JSON-Mode eignet sich nur, wenn ein Modell oder SDK-Pfad kein Schema unterstützt — als Fallback, nicht als Standard. Ein strict JSON Schema brauchen Sie, wenn Sie eine Antwort für Ihren eigenen Code oder einen Nutzer formen: Extraktion, Klassifikation, eine typisierte Zusammenfassung. Function Calling gehört dahin, wo das Modell selbst entscheidet, ob etwas ausgeführt wird. Aktivieren Sie den strict Mode auch für diese Tool-Definition, damit ein fehlerhafter Aufruf an der API scheitert und nicht in Ihrem Code.

Wenn Sie nur ein Label aus einem kurzen Enum brauchen, vergleichen Sie auch ein System-One-Modell. Ist die Ausgabe auf eine bekannte Menge von Antworten beschränkt, kann ein Modell eine davon auswählen, statt sie zu generieren, und die Kompromisse bei Latenz und Kosten unterscheiden sich genug, dass Sie sie an Ihrem eigenen Traffic messen sollten.

Für alles andere ist die Standardwahl ein strict Schema plus ein Validator auf dem geparsten Ergebnis. Das Schema hält die Form korrekt. Zu prüfen, ob der Inhalt stimmt, bleibt Ihre Aufgabe.

Verwandte Artikel