Du ba modellen om et JSON-objekt som inneholder fakturadata. Prompten var tydelig: "Return only valid JSON. No explanation." Det som kom tilbake var en markdown-code-fence, to setningers kommentar, et JSON-objekt — og så en hjelpsom note nederst som forklarte hvert felt. I produksjon, kl. 02.00, med en kundes datapipeline stanset. Hvis du bygger noe oppå LLM-API-er, kjenner du allerede denne smerten. LLM-er er ikke JSON-serialiserere. De er tekstgeneratorer som som regel produserer gyldig JSON — helt til de ikke gjør det. Denne artikkelen dekker de fem måtene de ødelegger den på, og de kamptestede mønstrene for å håndtere hver av dem.
De 5 måtene LLM-er ødelegger JSON
Dette er ikke edge case-er. Hver eneste av disse kommer til å skje deg i produksjon, som regel akkurat i det øyeblikket du slutter å sjekke etter dem.
- Markdown-code-fences — Modellen pakker JSON-en inn i
```json\n...\n```fordi treningsdataene dens er fulle av docs og README-filer som presenterer JSON på den måten. - Etterfølgende kommentarer — Modellen legger til en setning eller et avsnitt etter den avsluttende krøllparentesen: "Note: the
totalfield is in USD." - Truncation — Lange outputs blir kuttet midt i et objekt når svaret treffer tokengrensen, noe som etterlater deg med strukturelt ødelagt JSON uten avsluttende parenteser.
- Hallusinerte nøkler — Modellen finner opp feltnavn som ikke er i skjemaet ditt. Du ba om
invoice_number, du fikkinvoiceNumber,invoice_noogref_id— noen ganger i samme svar. - Feil typer — Tall kommer som strenger (
"49.99"i stedet for49.99), booleans som"true", arrays som kommaseparerte strenger. Type-coercion-bugs i forkledning.
Mønster 1: Stripp markdown-code-fences
Dette er den vanligste feilen og den enkleste å fikse. En enkel regex stripper fencen
uavhengig av om språktag-en er json, JSON, eller mangler helt.
Kjør den før all annen bearbeiding — den koster ingenting og forhindrer en stor klasse feil.
import re
def strip_code_fences(text: str) -> str:
"""Remove markdown code fences from LLM output."""
# Handles ```json, ```JSON, ``` (no lang tag), etc.
pattern = r'^```(?:json|JSON)?\s*\n?(.*?)\n?```$'
match = re.search(pattern, text.strip(), re.DOTALL)
if match:
return match.group(1).strip()
return text.strip()
# Example: model returned a fenced block
raw = """
```json
{
"invoice_number": "INV-2024-0192",
"vendor": "Acme Supplies",
"total": 1249.99,
"currency": "USD"
}
```
"""
clean = strip_code_fences(raw)
invoice = json.loads(clean) # now safefunction stripCodeFences(text) {
// Handles ```json, ```JSON, bare ``` (no lang), etc.
const match = text.trim().match(/^```(?:json|JSON)?\s*\n?([\s\S]*?)\n?```$/s);
return match ? match[1].trim() : text.trim();
}
// raw response contains a triple-backtick fence (shown here as a single-quoted string)
const raw = '```json\n{\n "invoice_number": "INV-2024-0192",\n "vendor": "Acme Supplies",\n "total": 1249.99\n}\n```';
const clean = stripCodeFences(raw);
const invoice = JSON.parse(clean); // safeMønster 2: Trekk ut JSON med regex
Når modellen legger til tekst før eller etter JSON-objektet — "Here is the
extracted data:", "Let me know if you need changes." — er det ikke nok å stripe fences. Du må
finne det ytterste {...}-blokken og trekke det ut. Trikset er å bruke et grådig match
som håndterer nestede objekter riktig. Merk at denne tilnærmingen håndterer objekter ({});
hvis skjemaet ditt er et array, bytt tegnklassen tilsvarende.
import re
import json
def extract_json_object(text: str) -> str | None:
"""
Extract the first complete JSON object from a string that may
contain surrounding prose or commentary.
"""
# Find the first { and last } to grab the outermost object
match = re.search(r'\{.*\}', text, re.DOTALL)
if not match:
# Fall back to array extraction if no object found
match = re.search(r'\[.*\]', text, re.DOTALL)
return match.group(0) if match else None
# Model returned prose + JSON + footnote
raw_response = """
Based on the document you provided, here is the structured data:
{
"invoice_number": "INV-2024-0192",
"vendor": "Acme Supplies",
"line_items": [
{"description": "Office chairs", "qty": 4, "unit_price": 299.99},
{"description": "Standing desk", "qty": 1, "unit_price": 649.99}
],
"total": 1849.95
}
Note: unit prices are pre-tax. Let me know if you need the tax breakdown.
"""
json_str = extract_json_object(raw_response)
if json_str:
invoice = json.loads(json_str)
print(f"Parsed invoice: {invoice['invoice_number']}")
else:
raise ValueError("No JSON object found in LLM response")Mønster 3: Bruk json-repair for strukturelle feil
Truncation og mindre strukturelle feil — en manglende avsluttende parentes, en usitert nøkkel, et
etterfølgende komma — er der regex-uttrekk kommer til kort.
json-repair-
biblioteket ble bygget nettopp for dette. Det bruker en rekke heuristikker for å gjenopprette så mye gyldig
struktur som mulig fra ødelagt JSON, på samme måte som nettlesere tolererer ugyldig HTML.
Installer det med pip install json-repair, og slipp det inn i parse-pipelinen din
som siste forsvarslinje før du gir opp på et svar.
import json
import json_repair # pip install json-repair
def parse_with_repair(text: str) -> dict | list | None:
"""
Attempt standard parse first; fall back to json_repair for
structurally broken responses (truncation, missing braces, etc.).
"""
# First pass: clean up fences and extract the JSON substring
cleaned = extract_json_object(strip_code_fences(text))
if not cleaned:
return None
# Second pass: try the fast standard parse
try:
return json.loads(cleaned)
except json.JSONDecodeError:
pass
# Third pass: let json_repair reconstruct broken structure
try:
repaired = json_repair.repair_json(cleaned, return_objects=True)
return repaired if repaired else None
except Exception:
return None
# Works even on truncated output from a token-limited response
truncated = """
{
"invoice_number": "INV-2024-0192",
"vendor": "Acme Supplies",
"line_items": [
{"description": "Office chairs", "qty": 4
"""
result = parse_with_repair(truncated)
# Returns {"invoice_number": "INV-2024-0192", "vendor": "Acme Supplies",
# "line_items": [{"description": "Office chairs", "qty": 4}]}Mønster 4: Prøv igjen med eksplisitt prompting
Noen ganger er den beste parseren selve modellen. Hvis outputtet er ødelagt utover det json-repair kan fikse — hallusinerte nøkler, helt feil struktur, et svar som er mer prosa enn data — send det ødelagte outputtet tilbake til modellen med parse-feilen og be den fikse sin egen feil. Modeller er overraskende gode på dette. Hold retry-antallet lavt (2–3 maks) og spor forsøk for å unngå uendelige løkker.
import json
from openai import OpenAI
client = OpenAI()
def call_model(messages: list) -> str:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
return response.choices[0].message.content
def extract_invoice_data(document_text: str, max_retries: int = 3) -> dict:
"""Extract structured invoice data with automatic retry on parse failure."""
system_prompt = """Extract invoice data and return ONLY a JSON object with these fields:
{
"invoice_number": string,
"vendor": string,
"issue_date": string (YYYY-MM-DD),
"due_date": string (YYYY-MM-DD) or null,
"line_items": [{"description": string, "qty": number, "unit_price": number}],
"subtotal": number,
"tax": number,
"total": number,
"currency": string (ISO 4217)
}
Return ONLY the JSON object. No markdown. No explanation."""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Extract invoice data from:\n\n{document_text}"}
]
for attempt in range(max_retries):
raw = call_model(messages)
try:
cleaned = extract_json_object(strip_code_fences(raw))
return json.loads(cleaned)
except (json.JSONDecodeError, TypeError) as e:
if attempt == max_retries - 1:
raise ValueError(
f"Failed to parse JSON after {max_retries} attempts. "
f"Last error: {e}. Last response: {raw[:200]}"
)
# Feed the error back — the model often corrects itself
messages.append({"role": "assistant", "content": raw})
messages.append({
"role": "user",
"content": (
f"That response caused a JSON parse error: {e}\n"
f"Please return ONLY a valid JSON object. No markdown fences, "
f"no commentary, just the raw JSON."
)
})
raise ValueError("Unexpected exit from retry loop")Mønster 5: Hopp over parsing — bruk Structured Outputs i stedet
Hvis du kontrollerer modell-kallet og har råd til å bruke nyere API-er, eliminerer structured outputs mesteparten av denne kompleksiteten helt. OpenAI Structured Outputs (tilgjengelig på GPT-4o og nyere) og Geminis response schema begrenser begge modellens output på token-genereringsnivået — det er matematisk umulig for modellen å returnere et ugyldig JSON-objekt fordi ugyldige tokens undertrykkes under dekoding. Ulempen: du gir opp litt av modellens kreativitet, og disse API-ene koster litt mer per kall. For høyvolums-ekstraksjonspipelines er de som regel verdt det.
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
class LineItem(BaseModel):
description: str
qty: int
unit_price: float
class Invoice(BaseModel):
invoice_number: str
vendor: str
issue_date: str # YYYY-MM-DD
total: float
currency: str # ISO 4217
line_items: list[LineItem]
def extract_invoice_structured(document_text: str) -> Invoice:
"""
Extract invoice using OpenAI Structured Outputs.
The API guarantees the response matches the Invoice schema —
no manual parsing or repair needed.
"""
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": "Extract invoice data from the provided document."
},
{"role": "user", "content": document_text}
],
response_format=Invoice
)
return completion.choices[0].message.parsed
invoice = extract_invoice_structured(document_text)
print(f"Invoice {invoice.invoice_number}: ${invoice.total:.2f} {invoice.currency}")En produksjonsklar parser (Python)
Slik ser en produksjonsklar ekstraksjonsfunksjon ut når du kombinerer alle fire defensive mønstre til ett verktøy. Dette er versjonen jeg faktisk kjører i tjenester som behandler tusenvis av LLM-svar per dag. Den stripper fences, trekker ut JSON-substrengen, forsøker en ren parse, faller tilbake til json_repair, og validerer valgfritt mot et JSON Schema før retur. Hvis du ikke bruker structured outputs, er dette fundamentet ditt.
import re
import json
from typing import Any
import json_repair # pip install json-repair
import jsonschema # pip install jsonschema
def strip_code_fences(text: str) -> str:
match = re.search(r'^```(?:\w+)?\s*\n?(.*?)\n?```$', text.strip(), re.DOTALL)
return match.group(1).strip() if match else text.strip()
def extract_json_substring(text: str) -> str | None:
match = re.search(r'\{.*\}', text, re.DOTALL) or re.search(r'\[.*\]', text, re.DOTALL)
return match.group(0) if match else None
def parse_llm_json(text: str, schema: dict | None = None) -> Any:
"""
Robustly parse JSON from LLM output.
Steps:
1. Strip markdown code fences
2. Extract outermost JSON object/array (handles surrounding prose)
3. Fast-path: standard json.loads
4. Slow-path: json_repair for structurally broken responses
5. Optional: validate against a JSON Schema
Args:
text: Raw text returned by the LLM
schema: Optional JSON Schema dict to validate the parsed result
Returns:
Parsed Python object (dict or list)
Raises:
ValueError: If parsing fails after all recovery attempts
jsonschema.ValidationError: If schema validation fails
"""
if not text or not text.strip():
raise ValueError("LLM returned an empty response")
# Step 1 — strip fences
text = strip_code_fences(text)
# Step 2 — extract JSON substring (handles prose before/after)
json_str = extract_json_substring(text)
if not json_str:
raise ValueError(f"No JSON object or array found in response: {text[:200]!r}")
# Step 3 — standard parse (fast path, no overhead)
parsed = None
try:
parsed = json.loads(json_str)
except json.JSONDecodeError as original_error:
# Step 4 — repair and retry
try:
repaired = json_repair.repair_json(json_str, return_objects=True)
if repaired is not None:
parsed = repaired
except Exception as repair_error:
raise ValueError(
f"JSON parse failed and repair also failed.\n"
f"Parse error: {original_error}\n"
f"Repair error: {repair_error}\n"
f"Input (first 500 chars): {json_str[:500]!r}"
) from original_error
if parsed is None:
raise ValueError(f"Parsing returned None for input: {json_str[:200]!r}")
# Step 5 — optional schema validation
if schema is not None:
jsonschema.validate(parsed, schema) # raises ValidationError on mismatch
return parsed
# --- Usage ---
INVOICE_SCHEMA = {
"type": "object",
"required": ["invoice_number", "vendor", "total"],
"properties": {
"invoice_number": {"type": "string"},
"vendor": {"type": "string"},
"total": {"type": "number"},
"currency": {"type": "string"},
"line_items": {"type": "array"}
}
}
llm_response = """
Sure! Here's the structured data:
```json
{
"invoice_number": "INV-2024-0192",
"vendor": "Acme Supplies",
"total": 1849.95,
"currency": "USD",
"line_items": [
{"description": "Office chairs", "qty": 4, "unit_price": 299.99}
]
}
```
Let me know if you need any changes!
"""
invoice = parse_llm_json(llm_response, schema=INVOICE_SCHEMA)
print(f"Vendor: {invoice['vendor']}, Total: ${invoice['total']}")JavaScript-versjon
Samme logikk i JavaScript. For reparasjonstrinnet er det nærmeste tilsvarende json_repair JSON5 for tolerant parsing av nesten-gyldig JSON, eller du kan skrive en lettvekts reparasjons-wrapper selv. For arbeid på klientsiden dekker JSON.parse() med et godt try/catch og en regex-fallback den store majoriteten av produksjonstilfeller.
// npm install json5 (optional — for tolerant parsing of near-valid JSON)
import JSON5 from 'json5';
function stripCodeFences(text) {
const match = text.trim().match(/^```(?:\w+)?\s*\n?([\s\S]*?)\n?```$/);
return match ? match[1].trim() : text.trim();
}
function extractJsonSubstring(text) {
// Greedy match for outermost object or array
const objectMatch = text.match(/\{[\s\S]*\}/);
if (objectMatch) return objectMatch[0];
const arrayMatch = text.match(/\[[\s\S]*\]/);
return arrayMatch ? arrayMatch[0] : null;
}
/**
* Robustly parse JSON from LLM output.
* Steps: strip fences → extract substring → JSON.parse → JSON5 fallback
*
* @param {string} text - Raw LLM response text
* @returns {object|Array} Parsed JavaScript value
* @throws {Error} If all parse attempts fail
*/
function parseLlmJson(text) {
if (!text || !text.trim()) {
throw new Error('LLM returned an empty response');
}
// Step 1 — strip markdown fences
let cleaned = stripCodeFences(text);
// Step 2 — extract JSON substring (skip surrounding prose)
const jsonStr = extractJsonSubstring(cleaned);
if (!jsonStr) {
throw new Error(`No JSON object or array found in response: ${text.slice(0, 200)}`);
}
// Step 3 — standard JSON.parse (fast path)
try {
return JSON.parse(jsonStr);
} catch (stdError) {
// Step 4 — JSON5 tolerant parser (handles trailing commas, unquoted keys, etc.)
try {
return JSON5.parse(jsonStr);
} catch (json5Error) {
throw new Error(
`JSON parse failed.\nStandard error: ${stdError.message}\nJSON5 error: ${json5Error.message}\nInput: ${jsonStr.slice(0, 300)}`
);
}
}
}
// --- Usage ---
const llmResponse = `
Here is the product data you requested:
\`\`\`json
{
"product_id": "SKU-8821-B",
"name": "Ergonomic Office Chair",
"price": 299.99,
"in_stock": true,
"tags": ["furniture", "ergonomic", "office"]
}
\`\`\`
Let me know if you need the full catalog!
`;
const product = parseLlmJson(llmResponse);
console.log(`Product: ${product.name} — $${product.price}`);
// → Product: Ergonomic Office Chair — $299.99Oppsummering
LLM-er ødelegger JSON på fem forutsigbare måter, og hver enkelt har en forutsigbar fiks. Markdown-fences
og omkringliggende prosa er kosmetikk — et par regex-er håndterer dem pålitelig. Strukturell skade fra
truncation eller mindre formateringsfeil er det
json_repair
ble bygget for. Når strukturen er riktig, men innholdet er feil — dårlige nøkler, feil typer — er det
et prompting-problem, og en retry-løkke med feilmeldingen matet tilbake til modellen er det beste verktøyet.
Og hvis du kan bruke
Structured Outputs,
gjør det — det eliminerer problemet ved kilden i stedet for å behandle symptomene. For ad hoc-
debugging når et spesifikt svar oppfører seg dårlig, vil JSON Fixer og
JSON Formatter spare deg tid. Bygg parse_llm_json-
verktøyet én gang, test det mot de verste historiske svarene dine, og gå videre — det finnes bedre
problemer å bruke debug-timene dine på.