Verlässliches JSON aus einem LLM – mit Structured Outputs statt Hoffnung
Antworte nur mit JSON" reicht nicht – irgendwann kommt doch ein "Klar, hier ist dein JSON:" davor und dein JSON.parse() stürzt ab. Mit einem JSON-Schema erzwingst du garantiert gültige Struktur.
Wer ein LLM programmatisch nutzt, will die Antwort meist als Datenstruktur, nicht als Fließtext – z. B. { "kategorie": "...", "dringlichkeit": 3 }. Der naive Weg ist, im Prompt „Antworte nur mit JSON" zu schreiben und das Ergebnis durch JSON.parse() zu jagen. Das geht so lange gut, bis das Modell doch ein „Klar, hier ist dein JSON:" davorsetzt oder ein Feld weglässt – und deine App abstürzt. Die saubere Lösung heißt Structured Outputs: Du gibst ein JSON-Schema vor, und die API garantiert eine dazu passende Antwort.
Ein Schema erzwingen (OpenAI)
// Läuft auf dem Server – der API-Key gehört nicht ins Frontend
async function classify(text) {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [{ role: 'user', content: `Klassifiziere: ${text}` }],
response_format: {
type: 'json_schema',
json_schema: {
name: 'ticket',
strict: true, // erzwingt exakt dieses Schema
schema: {
type: 'object',
properties: {
kategorie: { type: 'string', enum: ['bug', 'frage', 'lob'] },
dringlichkeit: { type: 'integer' },
},
required: ['kategorie', 'dringlichkeit'],
additionalProperties: false, // keine Extra-Felder
},
},
},
}),
})
const data = await res.json()
// Mit strict:true ist das immer gültiges, schema-konformes JSON
return JSON.parse(data.choices[0].message.content)
}
const t = await classify('Der Login hängt seit dem Update')
console.log(t.kategorie, t.dringlichkeit) // z. B. "bug" 3Warum das den Unterschied macht
strict: true+additionalProperties: falsegarantieren: keine Vorrede, keine fehlenden Pflichtfelder, keine erfundenen Zusatzfelder.JSON.parse()kann nicht mehr an unerwartetem Text scheitern.enumgrenzt Werte ein – so bekommst du nicht mal „Bug", „BUG" und „fehler" durcheinander, sondern genau deine drei erlaubten Kategorien.- Trotzdem defensiv bleiben: Bei einem Sicherheits-Refusal oder abgeschnittener Antwort (Token-Limit erreicht) kann das JSON ausbleiben. Ein
try/catchumJSON.parsegehört weiterhin dazu.
Fast jeder große Anbieter kennt dieses Muster: OpenAI über response_format, Claude (Anthropic) über output_config.format bzw. Tool-Use, Gemini über responseSchema. Das Prinzip ist überall gleich – Struktur erzwingen statt erhoffen.
Du willst KI-Ausgaben zuverlässig in deine App-Logik einbinden? Sprich mich an.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
ChatGPT-Antworten live streamen im Browser (OpenAI, fetch + SSE)
Statt auf die komplette KI-Antwort zu warten, lässt du sie Wort für Wort erscheinen – wie im echten ChatGPT. So liest du den Server-Sent-Events-Stream der OpenAI-API mit reinem fetch, ohne Bibliothek.
fetch() mit async/await und sauberem try/catch
Daten von einer API holen ist mit fetch() und async/await lesbar wie synchroner Code. Der Haken: fetch wirft bei 404 oder 500 keinen Fehler – du musst response.ok selbst prüfen.
Debounce in Vanilla JavaScript – ohne Bibliothek
Eine Suche, die bei jedem Tastendruck feuert, überlastet den Server. Mit einer kleinen debounce-Funktion wartest du, bis der Nutzer kurz Pause macht – in wenigen Zeilen, ganz ohne Framework.