FlyingFish Space
Back to Guides

Fixing Malformed JSON from LLM Output: A Field Guide to Every Common Error

AD
Developer Tool

JSON Pipeline โ€” A practical toolkit for working with JSON.

Clean, repair, transform and validate JSON without adding unnecessary friction.

JSON Clean AI Repair Transform Schema
Explore JSON Pipeline โ†’
Quick Reference

Syntax errors (fences, commas, quoting, truncation) -- safe to auto-fix, the intended text is recoverable from context
Missing or fabricated fields -- not safe to auto-fix with a guess; the correct move is to flag it, not invent it

You ask an LLM to return JSON. It works. You ship it. Then, at 2am, three weeks later, JSON.parse() throws on a response that looks completely normal in the logs. You stare at it for a full minute before you spot the problem: a trailing comma, hiding in plain sight after the last field.

If you've built anything on top of an LLM API and asked it for structured output, you've hit this. Even with a system prompt that says "output only valid JSON," even with JSON mode enabled, models still produce invalid JSON often enough that you need a repair strategy, not just a validator that tells you something's wrong.

This guide walks through every error pattern LLMs actually produce, in the order a repair pipeline should realistically try to fix them, with working code for each one. At the end, we'll cover a problem most "JSON repair" tools don't talk about: some of them don't just fix syntax -- they fill in missing data with a guess, and that's a very different kind of "fix."

1. The Problem: Why "Just Ask for JSON" Isn't Enough

An LLM is a text generator. Valid JSON is a side effect of that text happening to be well-formed -- it's not a guarantee enforced by the model itself, unless you're specifically using a constrained/structured-output mode that enforces a grammar at generation time. Even then, plenty of production pipelines call chat completion endpoints directly, without structured output enforcement, and get back something like this:

Sure! Here's the JSON you asked for:

```json
{
  "name": "Alice",
  "age": 30,
  "tags": ["admin", "billing"],
}
```

Let me know if you need anything else!

Three separate problems in one response: conversational prose wrapping the answer, a markdown code fence, and a trailing comma after the last array element. None of these are edge cases -- they're the default behavior of a model that was trained to sound helpful and write code the way humans write code, trailing commas included.

2. The Cause: Eleven Failure Patterns, One Root Cause

Every LLM JSON failure traces back to the same root cause -- the model is pattern-matching "what JSON-like text looks like" from its training data, which includes JavaScript object literals (which permit trailing commas and unquoted keys), Markdown-formatted answers (which wrap code in fences), and conversational writing (which adds a sentence before and after the "answer"). None of that is JSON-illegal from the model's perspective, because most of it isn't illegal in the contexts it learned from.

Here is the realistic order to detect and fix these, from "doesn't even require touching the string content" to "requires structural repair":

2.1 Non-JSON Wrapping

  • Markdown code fences: ```json ... ```
  • Conversational prose before or after: "Sure! Here's your JSON: {...}"
  • Byte-order marks and stray control characters from copy-paste or streaming artifacts

2.2 Encoding Artifacts

  • Smart/curly quotes (โ€˜ โ€™ โ€œ โ€) instead of straight ones -- common when a model has been "writing prose" in the same turn
  • En-dashes and em-dashes where a hyphen was intended
  • Non-breaking spaces that look like regular spaces but aren't
  • Orphaned Unicode surrogate pairs from truncated multi-byte sequences

2.3 Illegal Literals

  • Illegal escape sequences inside string values
  • NaN, Infinity, -Infinity -- valid in JavaScript, illegal in JSON, and models mix the two constantly

2.4 Structural Syntax Errors

  • JavaScript-style // and /* */ comments
  • Trailing commas before } or ]
  • Unquoted or single-quoted object keys and string values

2.5 Truncation

  • The response gets cut off mid-object or mid-array because of a token limit, leaving unclosed brackets

Notice what's not on this list: a field that's missing entirely, or a value that's just wrong. Those aren't syntax problems -- they're semantic problems, and fixing them requires guessing, which is a fundamentally different kind of "fix" (more on that in section 5).

3. The Solution: A Repair Pipeline That Tries Cheap Fixes First

The efficient approach isn't one giant regex -- it's a staged pipeline that attempts progressively more invasive fixes, re-testing with a real parse after each stage, and stopping the moment a valid parse succeeds.

3.1 JavaScript / Node.js -- A Minimal Staged Pipeline

function repairJson(raw) {
  let text = raw;

  // Stage 1: strip BOM and non-printable control characters
  text = text.replace(/^\uFEFF/, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');

  // Stage 2: pull JSON out of a markdown fence or surrounding prose
  const fence = text.trim().match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
  if (fence) {
    text = fence[1].trim();
  } else {
    const start = text.search(/[{\[]/);
    if (start > 0) text = text.slice(start);
  }

  // Stage 3: normalize smart quotes and dashes
  text = text
    .replace(/[\u2018\u2019]/g, "'")
    .replace(/[\u201C\u201D]/g, '"')
    .replace(/\u2013/g, '-')
    .replace(/\u2014/g, '--');

  // Stage 4: JS-only literals JSON doesn't allow
  text = text.replace(/-?Infinity\b|NaN\b/g, 'null');

  // Try a cheap parse before doing anything more invasive
  try { return JSON.parse(text); } catch {}

  // Stage 5: strip comments, fix trailing commas, quote bare keys
  text = text.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g, '');
  text = text.replace(/,\s*([}\]])/g, '$1');
  text = text.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)/g, '$1"$2"$3');

  try { return JSON.parse(text); } catch {}

  // Stage 6: close unclosed brackets (truncated response)
  const stack = [];
  let inString = false;
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (inString) {
      if (ch === '\\') { i++; continue; }
      if (ch === '"') inString = false;
    } else {
      if (ch === '"') inString = true;
      else if (ch === '{') stack.push('}');
      else if (ch === '[') stack.push(']');
      else if (ch === '}' || ch === ']') stack.pop();
    }
  }
  text += (inString ? '"' : '') + stack.reverse().join('');

  return JSON.parse(text); // let this throw if truly unrecoverable
}

3.2 Python -- Same Strategy

import json
import re

def repair_json(raw: str):
    text = raw.strip()

    # Strip markdown fences
    fence = re.match(r'^```(?:json)?\s*\n?(.*?)\n?```$', text, re.DOTALL | re.IGNORECASE)
    if fence:
        text = fence.group(1).strip()
    else:
        match = re.search(r'[{\[]', text)
        if match and match.start() > 0:
            text = text[match.start():]

    # Normalize smart quotes
    text = (text.replace('\u2018', "'").replace('\u2019', "'")
                .replace('\u201c', '"').replace('\u201d', '"'))

    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    # Remove trailing commas
    text = re.sub(r',\s*([}\]])', r'\1', text)
    # Quote bare keys
    text = re.sub(r'([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)', r'\1"\2"\3', text)

    return json.loads(text)  # raise if still unrecoverable

3.3 Prompting to Reduce How Often You Need Any of This

The cheapest fix is preventing the error from being generated in the first place. This won't get you to zero, but it measurably reduces the failure rate:

SYSTEM_PROMPT = """
Output ONLY raw JSON. No markdown code fences. No explanation before
or after. No trailing commas. All keys and string values must use
double quotes.
"""

// Combine with your provider's structured-output / JSON mode when
// available -- prompting reduces the failure rate, structured output
// modes enforce a grammar at generation time and get you much closer
// to zero, though neither one eliminates the class of errors above
// entirely for every provider and model version.

4. Common Pitfalls

Pitfall #1: Fixing in the Wrong Order

Stripping comments before extracting the JSON block can accidentally eat part of a URL inside a string value ("https://example.com" contains //). Always extract the JSON block and quote-aware-strip comments before running comma/quoting fixes, and make sure your comment-stripping regex skips over string literals rather than blindly matching // anywhere in the text.

Pitfall #2: Retrying the Same Prompt Instead of Repairing

A common but expensive pattern: catch the parse error, then re-call the LLM with the same prompt and hope for a better roll. This doubles your latency and cost for a class of errors that's almost always mechanically fixable in milliseconds. Repair first; only retry the generation if repair genuinely fails.

Pitfall #3: Treating a Successful Repair as a Correctness Guarantee

A repaired JSON string that parses without error tells you the syntax is now valid -- it says nothing about whether the values are the ones you actually wanted. Syntax repair and semantic validation (does this data make sense? are required fields present?) are two separate steps, and skipping the second one is how a bad number sails straight into your database looking perfectly innocent.

Pitfall #4: Assuming Truncation Always Means "Just Close the Brackets"

Stack-based bracket completion works when the model got cut off between elements. It does not recover a truncated string value mid-word, or a field that was cut off before its value was written at all -- those cases need to be detected and either flagged or dropped, not silently "completed" with a guess at what the rest of the string might have been.

5. The Fix Most Tools Don't Mention: What Happens to Missing Fields

Here's a detail worth knowing before you pick a repair tool: several popular JSON repair libraries, when they encounter a field that's referenced but missing a value, will automatically fill it in with a "reasonable default" -- an empty string, a zero, a placeholder -- so the output parses cleanly. That's a completely reasonable design choice for some use cases (you genuinely just need something parseable). But it means the tool isn't just fixing syntax anymore -- it's making a decision about what your data should contain, silently, and that decision looks identical in the output whether it's a smart guess or a wrong one.

If a required field is genuinely absent from a model's raw output, a repair layer has exactly two honest options: leave it absent and let your own validation catch it, or refuse the repair and tell you why. Inventing a plausible-looking value is the one option that turns a visible problem (a missing field, easy to catch) into an invisible one (a fabricated field, indistinguishable from real data until something downstream breaks).

6. Practical Checklist

Step 1: Classify the Failure

  • โ˜ Does it fail to parse at all? โ†’ syntax repair
  • โ˜ Does it parse but a required field is missing? โ†’ semantic problem, do not auto-fill
  • โ˜ Does it parse but a value looks wrong? โ†’ semantic problem, needs validation against a schema, not a repair tool

Step 2: Try the Cheap Fixes First

  • โ˜ Strip markdown fences and leading/trailing prose
  • โ˜ Normalize smart quotes, dashes, non-breaking spaces
  • โ˜ Re-parse after each stage -- stop as soon as it succeeds

Step 3: Handle Structural Damage

  • โ˜ Trailing commas, unquoted keys, single-quoted strings
  • โ˜ JavaScript-only literals (NaN, Infinity, comments)
  • โ˜ Truncated structures -- close open brackets, but check whether a string value itself got cut off first

Step 4: Validate After Repairing

  • โ˜ Run the repaired JSON through a schema check, not just a parser
  • โ˜ Confirm every required field is present and typed correctly
  • โ˜ Reject rather than silently accept if a required field is still missing after repair

Step 5: Reduce Future Failures

  • โ˜ Tighten the system prompt (no fences, no prose, no trailing commas)
  • โ˜ Use your provider's structured-output mode where available
  • โ˜ Log which failure patterns are actually occurring in production -- the mix varies by model and provider

7. Diagnostic Tool

function diagnoseJsonFailure(raw) {
  const checks = [
    { name: 'Markdown fence', test: /```/.test(raw) },
    { name: 'Leading prose before { or [', test: /^[^{\[]*\S[^{\[]*[{\[]/.test(raw.trim()) },
    { name: 'Trailing comma', test: /,\s*[}\]]/.test(raw) },
    { name: 'Single-quoted string', test: /(? console.log(c.test ? 'โš ๏ธ' : 'โœ…', c.name));

  try {
    JSON.parse(raw);
    console.log('โœ… Parses as-is -- no repair needed');
  } catch (e) {
    console.log('โŒ Fails to parse:', e.message);
  }
}

diagnoseJsonFailure('```json\n{name: \'Alice\', tags: ["a",],}\n```');

Final Thoughts

Almost every JSON error an LLM produces falls into one of eleven predictable patterns, and almost all of them are mechanically, safely fixable -- they're recovering intent the model clearly had, just expressed in a form the JSON spec doesn't allow. The one thing that isn't safe to auto-fix is a field that's genuinely missing. Filling that in with a guess isn't a repair -- it's the tool quietly making up part of your data and handing it back to you looking exactly as trustworthy as the rest.

The rule of thumb worth keeping: fix the syntax freely. Never let a repair step invent a value for something the model never actually produced.

๐Ÿ“– You Might Also Like