Published on

Claude: gültiges JSON erzwingen statt reparieren

Authors

Strukturierte Ausgaben bei Claude

TL;DR

Claude kennt zwei Wege, gültige Struktur zu garantieren: output_config.format für die Antwort und strict: true für Werkzeugparameter. Beide erzwingen die Struktur, nicht die Richtigkeit — und genau diese Verwechslung macht Fehler unsichtbarer, statt sie zu beseitigen.


Der Reparaturcode, den man nicht mehr braucht

# Die Variante, die man ersetzen kann
text = antwort.content[0].text.strip()
if text.startswith("```"):
    text = text.split("```")[1].removeprefix("json")
try:
    daten = json.loads(text)
except json.JSONDecodeError:
    daten = notfall_parsen(text)

Dieser Code existiert, weil ein Modell, das um JSON gebeten wird, manchmal einen einleitenden Satz voranstellt, Code-Zäune setzt oder eine Klammer vergisst. Bei einem interaktiven Assistenten ist das ärgerlich; in einem Ablauf, der 4.000 Rechnungen verarbeitet, ist es ein Betriebsproblem.

Weg 1: das Antwortformat erzwingen

Der empfohlene Weg führt über messages.parse() mit einem Pydantic-Modell — die Validierung passiert dabei automatisch:

from pydantic import BaseModel, Field
from typing import Literal, List

class Position(BaseModel):
    artikelnummer: str
    bezeichnung: str
    menge: float
    einzelpreis_eur: float

class Rechnung(BaseModel):
    lieferant: str
    rechnungsnummer: str
    rechnungsdatum: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}$")
    waehrung: Literal["EUR", "CHF", "USD"]
    positionen: List[Position]
    netto_gesamt: float

antwort = client.messages.parse(
    model="claude-opus-5",
    max_tokens=16000,
    messages=[{"role": "user", "content": f"Extrahiere die Rechnungsdaten:\n\n{text}"}],
    output_format=Rechnung,
)

rechnung = antwort.parsed_output      # bereits validiertes Rechnung-Objekt

Ohne Pydantic geht es über das Rohschema:

antwort = client.messages.create(
    model="claude-opus-5",
    max_tokens=16000,
    messages=[...],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "lieferant": {"type": "string"},
                    "netto_gesamt": {"type": "number"},
                },
                "required": ["lieferant", "netto_gesamt"],
                "additionalProperties": False,     # Pflicht
            },
        }
    },
)

Ein Hinweis für bestehenden Code: Der frühere Parameter output_format auf messages.create() ist veraltet. Auf messages.create() gehört das Schema unter output_config.format; output_format bleibt als Bequemlichkeitsparameter auf messages.parse() zulässig.

Weg 2: strikte Werkzeugparameter

Wenn nicht die Antwort, sondern der Werkzeugaufruf verlässlich sein muss, setzen Sie strict — als Feld der Werkzeugdefinition, nicht bei tool_choice:

tools=[{
    "name": "flug_buchen",
    "description": "Bucht einen Flug zu einem Ziel.",
    "strict": True,                              # hierhin, nicht zu tool_choice
    "input_schema": {
        "type": "object",
        "properties": {
            "ziel": {"type": "string"},
            "datum": {"type": "string", "format": "date"},
            "passagiere": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6]},
        },
        "required": ["ziel", "datum", "passagiere"],
        "additionalProperties": False,
    },
}]

Damit ist garantiert, dass tool_use.input exakt dem Schema entspricht — Sie brauchen im Werkzeug keine Strukturprüfung mehr.

Das Kleingedruckte am Schema

Nicht jedes JSON-Schema wird unterstützt. Was funktioniert:

Grundtypen, enum, const, anyOf, allOf, $ref/$def, die Zeichenkettenformate date-time, date, time, duration, email, hostname, uri, ipv4, ipv6, uuid — und additionalProperties: false, das für jedes Objekt gesetzt sein muss.

Was nicht funktioniert:

  • rekursive Schemata
  • numerische Grenzen wie minimum, maximum, multipleOf
  • Längenbegrenzungen wie minLength, maxLength
  • komplexe Array-Bedingungen
  • additionalProperties mit einem anderen Wert als false

Die Python- und TypeScript-SDKs entfernen nicht unterstützte Bedingungen automatisch aus dem Schema und prüfen sie stattdessen clientseitig — was praktisch ist und bedeutet, dass eine Grenze wie maximum: 100 erst nach der Erzeugung greift, nicht währenddessen.

Was das Erzwingen nicht leistet

Der Vorbehalt, der in Anleitungen fehlt und der wichtiger ist als alles oben.

Erzwungen wird die Struktur, nicht die Richtigkeit. Ein Modell, das den Betrag falsch liest, liefert einen falschen Betrag in gültigem JSON. Vorher scheiterte die Verarbeitung an einem Parse-Fehler — jetzt läuft sie mit falschen Daten durch. Sie haben die sichtbaren Fehler beseitigt und die inhaltlichen unsichtbarer gemacht.

Ergänzen Sie deshalb fachliche Prüfungen, die das Schema nicht abbilden kann:

summe = sum(p.menge * p.einzelpreis_eur for p in rechnung.positionen)
if abs(summe - rechnung.netto_gesamt) > 0.02:
    zur_pruefung(rechnung, grund=f"Summe {summe:.2f} != {rechnung.netto_gesamt:.2f}")

Fünf Zeilen, die in der Praxis mehr echte Fehler fangen als jede Schemaverschärfung.

Ein zu enges Schema erzeugt Halluzination. Wenn Sie ein Feld als Pflicht deklarieren, das im Dokument gar nicht vorkommt, füllt das Modell es trotzdem — mit etwas Erfundenem. Machen Sie optionale Felder auch im Schema optional.

Drei Einschränkungen im Betrieb

Der erste Aufruf mit einem neuen Schema ist langsamer — es wird einmalig übersetzt und danach 24 Stunden zwischengespeichert. Bei selten genutzten Schemata zahlen Sie diese Latenz regelmäßig.

Nicht kombinierbar mit Quellenangaben. output_config.format und aktivierte Zitate ergeben zusammen einen Fehler 400.

Bei stop_reason: "max_tokens" ist die Ausgabe unvollständig — auch wenn sie strukturiert erzeugt wurde. Und bei stop_reason: "refusal" entspricht sie unter Umständen nicht dem Schema. Prüfen Sie stop_reason, bevor Sie den Inhalt lesen.

Häufig gestellte Fragen

Wie bringe ich Claude dazu, zuverlässig gültiges JSON zu liefern?

Über output_config.format mit einem JSON-Schema, oder komfortabler über messages.parse() mit einem Pydantic-Modell. Die Struktur wird dann während der Erzeugung erzwungen, sodass ungültiges JSON gar nicht entstehen kann — Reparaturcode und try-Blöcke entfallen.

Garantiert erzwungenes JSON auch inhaltlich richtige Werte?

Nein, und diese Verwechslung ist gefährlich. Erzwungen wird ausschließlich die Struktur; ein falsch gelesener Betrag steht danach als gültige Zahl im gültigen JSON. Ergänzen Sie fachliche Prüfungen wie Summenproben — sie fangen mehr echte Fehler als jede Schemaverschärfung.

Welche JSON-Schema-Merkmale werden nicht unterstützt?

Rekursive Schemata, numerische Grenzen wie minimum und maximum, Längenbegrenzungen wie minLength, komplexe Array-Bedingungen und additionalProperties mit einem anderen Wert als false. Die Python- und TypeScript-SDKs entfernen solche Bedingungen automatisch und prüfen sie clientseitig nach der Erzeugung.

Wohin gehört das Feld strict bei Werkzeugen?

Als Feld auf oberster Ebene der Werkzeugdefinition, neben name, description und input_schemanicht bei tool_choice. Das Schema muss dabei additionalProperties: false und ein required-Feld enthalten. Damit ist garantiert, dass die Werkzeugeingabe exakt dem Schema entspricht.

Warum ist der erste Aufruf mit einem neuen Schema langsamer?

Weil das Schema einmalig übersetzt wird; danach greift ein Zwischenspeicher mit 24 Stunden Gültigkeit. Bei häufig genutzten Schemata fällt das nicht ins Gewicht, bei selten genutzten zahlen Sie die Latenz regelmäßig — was ein Argument dafür ist, die Zahl unterschiedlicher Schemata klein zu halten.


Der nächste Schritt

Suchen Sie in Ihrem Code nach dem Muster aus Code-Zaun-Entfernung und try-Block um json.loads. Jede Fundstelle lässt sich durch eine Schema-Angabe ersetzen — und ergänzen Sie im selben Zug eine fachliche Plausibilitätsprüfung, denn die brauchen Sie danach erst recht. Bei der Umstellung helfen wir gern.

📖 Verwandte Artikel

Weitere interessante Beiträge zu ähnlichen Themen