Skip to main content

Fix Broken AI JSON in Your Browser: A Developer's Tutorial

Updated 7 min readMarkdown
Fix Broken AI JSON in Your Browser: A Developer's Tutorial

Take a mangled LLM payload — fences, trailing commas, smart quotes, Python-style booleans — and walk it to valid JSON defect by defect, all client-side.

Copy the model's output. Paste it into your config. JSON.parse dies at position 312 and your deploy goes red. This is the daily tax of working with LLM-generated JSON — and it's fixable in about five minutes, in your browser, with the data never leaving the tab.

This post is a hands-on repair session, not a theory piece. I'll take one realistic payload straight out of a chat model — fences, trailing commas, smart quotes, comments, Python-style booleans and all — and walk it to a valid document defect by defect. Along the way I'll explain why the model made each mistake (it's token-level, and it's predictable) and why the whole fixer should run client-side. The workflow below mirrors the browser-local JSON validator I maintain, but the repair steps are yours to run anywhere — including by hand.

The specimen: one broken payload

The model returns this, with its usual friendly markdown wrapper:

```json
{
  "name": "checkout-service",
  "replicas": 3,
  "image": "registry.example.com/checkout:v2.4.1",
  "env": {
    "LOG_LEVEL": "debug",
    "RETRY_LIMIT": 5,
    "FEATURE_FLAGS": ["new-tax-flow", "retry-backoff",],
    // copied from the staging config — TODO: verify before prod
    staging_only: True,
  },
  "health": {
    'path': "/healthz",
    "interval_seconds": 30,
  },
  "notify": "Ping the “payments-oncall” channel if deploy stalls",
}
```

Count the defects: seven, all real, all common. Let's fix them in a safe order — outermost and least risky first, re-validating after each step. The principle is simple: every repair must move the parser's error position forward. If the error moves backward, you broke something — roll back that step.

Step 1. Strip the markdown code fences

Delete the opening ```json line and the closing ```. The fence is markdown semantics, not JSON, and parsers choke on the very first backtick.

Why the model does it: instruction tuning. Human feedback consistently rewards wrapping code in fenced blocks, so the model applies the habit to JSON output by default — even in "JSON mode."

Step 2. Normalize smart quotes to straight quotes

Look at the notify value: the quotes around payments-oncall are “ ” (U+201C/U+201D), not ASCII " (U+0022). In most fonts they render identically. To the parser they are ordinary characters, and the string boundary logic falls apart.

Why this happens: a typographic normalization layer between the model and you — the chat UI, a docs editor, or a CMS that "helpfully" converts quotes. Sometimes the model itself emits them after training on prose. Either way, normalize U+201C/U+201D → " and U+2018/U+2019 → ' before any delimiter surgery, because every later regex needs to know where strings actually start and end.

Step 3. Remove the trailing commas

Two of them here: one after "retry-backoff" in the array, one after "interval_seconds": 30 in the health object.

Why the model does it: probability, not spite. The model's training corpus is dominated by JavaScript object literals, Python dicts, and Rust structs — all of which tolerate or encourage trailing commas. At generation time, the token , after a value is simply more probable than the bare } or ] the JSON grammar demands. The model optimizes for the most likely next token, not the strictest one.

The fix: delete commas that directly precede } or ] — but only outside string literals. A naive regex like /,\s*}/g works on this payload, but it will eat commas inside string values too — respect string boundaries (Step 2 first), or you'll create new bugs while fixing old ones.

Step 4. Quote the unquoted keys, fix the single quotes

staging_only: True needs its key quoted; 'path' needs its single quotes converted to double.

Why the model does it: JavaScript object literals don't require quoted keys, and single-quoted strings are idiomatic in both JS and Python. The model learned the strict JSON form and the looser forms in the same weights, and the looser forms leak in — especially in long outputs where attention drifts from the format constraint. This one gets worse the longer the payload, which is exactly when you least want to debug it by hand.

Step 5. Convert the Python-isms

True is not true. Neither is False → false, nor None → null.

Why the model does it: the training data is multilingual in a deeper sense than natural languages — it mixes programming languages too. The format constraint ("output JSON") is weak compared to the prior ("when I saw a boolean in config-like text, it was often True"). This is the defect that betrays the token-level truth most cleanly: the model isn't generating JSON; it's generating text that resembles JSON from all the languages it knows.

Step 6. Delete comments and trailing prose

The // copied from the staging config line has to go. Real LLM outputs also tack on prose after the closing brace — "Let me know if you need anything else!" — which must be cut.

Why the model does it: it trained on config files where comments are normal (JSONC, YAML) and on explanatory answers where prose follows the code block. Both habits are outside the JSON grammar. Strip // and /* */ comments only outside strings — same discipline as Step 3.

The repaired document

After all six steps, re-validation passes. The final document:

{
  "name": "checkout-service",
  "replicas": 3,
  "image": "registry.example.com/checkout:v2.4.1",
  "env": {
    "LOG_LEVEL": "debug",
    "RETRY_LIMIT": 5,
    "FEATURE_FLAGS": ["new-tax-flow", "retry-backoff"],
    "staging_only": true
  },
  "health": {
    "path": "/healthz",
    "interval_seconds": 30
  },
  "notify": "Ping the \"payments-oncall\" channel if deploy stalls"
}

Note the notify value needed one more judgment call: after Step 2 normalized the smart quotes to straight quotes, the inner quotes around payments-oncall would have terminated the string early. Quote normalization is necessary but not sufficient — the correct repair is escaping them (\"), which is a semantic decision, not a mechanical one. This is why the workflow ends with preview before accept: mechanical fixes produce a proposed document, you review the diff, and only then accept it. Silent mutation is how you ship a "fixed" config that points at the wrong channel.

One more defect class worth a flag while you're here: duplicate keys. "role": "admin", "role": "user" parses without an error — the parser silently keeps the last value and you lose data. A good fixer warns on it even though it's "valid" syntax. Also check number precision if the payload crosses language boundaries: large integers that parse fine in V8 can lose precision in float64-based consumers.

Why the fixer has to run in your browser

Consider what this payload actually contains: internal hostnames, feature flags, on-call channel names. Real-world broken JSON is rarely a toy config — it's API keys pasted into a prompt for debugging, customer records being transformed, internal schemas. Pasting that into a server-side "AI JSON fixer" trades a syntax problem for a data-exfiltration problem.

That's the argument for doing the whole pipeline locally: syntax check, location-aware errors, duplicate-key warnings, repair preview, even JSON Schema validation — all inside the browser tab, with the document never leaving it. The JSON validator I work on is built exactly this way, and the claim is falsifiable by design: open the Network panel while you repair, and watch nothing leave. "Your JSON stays in this browser" isn't a tagline there; it's the architecture. Remote $ref values are never loaded either, so even schema validation doesn't phone home.

Treat LLM output as hostile input

The pattern above is the durable one: validate first, fix in a safe order, re-validate after every step, preview before accepting, and keep the data local throughout. LLMs will keep emitting almost-JSON — that's not a prompting problem you can fully solve, it's a property of next-token prediction trained on non-strict data. About five minutes per payload, with the error position always moving forward.

Full disclosure: I built ToolkitLoop, including this JSON validator.

About the author

  • Founder of ToolkitLoop (toolkitloop.com) — 30 free online tools that run 100% in your browser. No sign-ups, no uploads, private by design.

No comments yet. Be the first to comment.