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
toSorted, toReversed und with: Arrays sortieren, ohne das Original zu zerstören
array.sort() sortiert nicht eine Kopie, sondern das Array selbst – und genau daran ist schon mancher Zustand im Frontend stillschweigend kaputtgegangen. Seit ES2023 gibt es zu jeder dieser Methoden eine Variante, die eine Kopie zurückgibt.
Aufzählungen richtig verketten mit Intl.ListFormat
"a, b und c" von Hand zusammenzubauen geht fast immer schief – bei einem Element, bei zwei Elementen, beim letzten Komma. Intl.ListFormat macht es richtig, in jeder Sprache, ganz ohne Bibliothek.
requestAnimationFrame – die Animationsschleife, die zum Bildschirm passt
setInterval(schritt, 16) läuft auf einem 144-Hz-Monitor anders als auf einem alten Handy – und im Hintergrundtab einfach weiter. requestAnimationFrame löst beides, wenn man die verstrichene Zeit mitrechnet statt Bilder zu zählen.