Why Does AI-Generated JSON Fail JSON.parse()? Complete Causes and Fixes

As of 17 September 2026: feeding a chat reply to JSON.parse() usually fails because of fences, wrapping prose, trailing commas, truncation, and JS dialect — not because the model cannot write JSON. A cause map and a fix order.

Up front: a JSON.parse() error usually does not mean “the model cannot write JSON.” It means you fed an entire chat reply to a parser that accepts one JSON value.JSON.parse accepts a single value in the JSON grammar (ECMA-262 / RFC 8259). Markdown fences, wrapping prose, trailing commas, raw newlines, truncation, and JS/Python dialect all throw SyntaxError immediately. The 2026 fix order is: if you can use Structured Output or read Tool Calling arguments, do not parse chat prose; if you must parse, extract, then parse, then validate with JSON Schema — do not start by regex-repairing until it “kind of parses.”

Written as of 17 September 2026. This site already has What Structured Output is, From prompt to Structured Output, OpenAI vs Gemini Structured Output, structured JSON with the Gemini API, and Tool Calling and JSON Schema. This piece only answers why model output fails JSON.parse, and in what order to fix it.

What JSON.parse actually accepts

In browsers and Node, JSON.parse implements JSON text, not “a JavaScript object literal that looks close enough.” Whitespace (space, tab, line feed, carriage return) may surround the value. Otherwise the input must be exactly one value: object, array, string, number, true / false / null. Non-whitespace after that value fails — Chrome often says Unexpected non-whitespace character after JSON.

These forms run in JS and die in JSON. Models copy them from training data all the time:

FormJS object / JSON5JSON.parse
Trailing comma{"ok": true,} okthrows
Single quotes{'ok': true} okthrows
Comments// note okthrows
Bare keys{ok: true} okthrows
undefined / NaN / Infinityexist in the languagethrows
Raw newline in a stringtemplate strings allow itthrows; must be \n

Debug with one question: did you pass “one JSON value,” or “a paragraph the model wrapped for readability”? The parser owns the first. You must extract the second.

A table of failure classes

Classify the SyntaxError before you wrestle the exact wording. Chrome, Safari, and Node phrase the same bug differently. The classes are few:

ClassWhat the model often emitsTypical resultDo this first
Wrapper```json fences, “here is the JSON”First character is not { / [Strip the fence, then slice a balanced value
DialectTrailing commas, single quotes, comments, bare keysUnexpected tokenSwitch to Structured Output; do not parse as JS
Broken stringUnescaped ", raw newlines, fullwidth commasString ends early, or no : after a keyRead the column; cap field length
TruncationUnclosed object or arrayUnexpected end of JSON inputRaise the output cap; wait for the stream
Multiple valuesTwo JSON values, or prose after the firstCharacters after the first valueSlice only the first complete value
EncodingBOM, zero-width chars, double stringifyOdd token, or parse yields a stringStrip BOM; check typeof before parsing again

For agents, add one more: Tool Calling arguments are often already an object, or a JSON string the vendor already constrained. Do not run the whole assistant message through JSON.parse. That is a different channel — see why Tool Calling depends on JSON Schema.

Fences and wrapping prose

Chat models are trained to put code in fences. Even if you wrote “JSON only,” the reply is often:

```json
{"ok": true, "id": "A-1024"}
```
Here is the result. I can explain the fields if you want.

The first character is a backtick, not {. JSON.parse fails on column 0. A leading “Sure, here is the JSON:” or a trailing disclaimer is the same bug. Worse: two values — a sample, then the real result. Parse the whole blob and you die after the first }.

Extract with one rule: find the first balanced {} or [] (skip brackets inside strings) and pass only that slice to JSON.parse. Strip fences first. Do not greedily cut from the first { to the last } — brackets inside strings, or a second object in the explanation, will slice wrong.

Dialect: trailing commas, single quotes, comments, bare keys

Models have seen masses of JavaScript, Python, JSON5, and YAML. When asked for “structured data,” they mix dialects. All of the following are illegal for JSON.parse:

{
  ok: true,          // bare key + comment
  'name': 'Ada',     // single quotes
  "tags": ["a",],    // trailing comma
  "flag": True       // Python boolean
}

Add undefined, NaN, Infinity, None. They mean something in their languages; JSON has null and finite numbers. Swapping JSON.parse for eval or new Function to “accept” these forms turns the parser into an arbitrary-code sink. Do not do that in production.

JSON5 and JSONC can swallow comments and trailing commas. That is fine for humans editing config. It is a poor default parser for model output. Once you loosen the grammar, you can no longer tell “extra comma” from “a broken string.” If you need a loose layer, keep it behind extract + parse failure, and still run Schema after a repair.

Strings and punctuation: escapes, newlines, fullwidth and smart quotes

A legal JSON string uses double quotes. Inner " and backslashes must be escaped. Control characters must be \n, \t, or \uXXXX. When a model copies a user comment, raw quotes and newlines land inside the field. The string ends early; the next comma or CJK character becomes an unexpected token.

CJK output adds a frequent dirt set: fullwidth comma ,, fullwidth colon :, and curly quotes “” / ‘’. They look like punctuation; their code points are not 0x2C / 0x3A / 0x22. This “almost JSON” dies after the name value:

{
  "name": "Ada",
  "ok": true
}

Do not fix this with another “please use ASCII punctuation” sentence. Put maxLength on long string fields, have the model cite source text instead of retyping punctuation, and use Structured Output on the final channel. For debugging, paste into the JSON validator and see which column the highlight stops on — a fullwidth comma is obvious.

Truncation and streaming: Unexpected end of JSON input

Unexpected end of JSON input almost always means the text ended before the grammar did: missing }, missing ], or an unclosed string. In 2026 the usual sources are an output token cap, a safety cut, or you called JSON.parse on an incomplete stream chunk.

A streaming API gives you deltas. Early chunks may be {"ok": tr. Parse that and you will fail. Do this instead:

  • Wait for the stream to finish (finish_reason / stop), then parse the full buffer;
  • Or use a real streaming JSON parser that advances token by token — do not call JSON.parse on a half value;
  • If the stop reason is length / max_tokens, this is not a parse bug. Generation did not finish — raise the cap, shrink the Schema, or page the model.

Auto-closing braces after a truncation is a draft trick. The shape may parse and still miss fields or cut a string in half. After any repair, Schema-validate; on failure, retry. Do not silently store it.

Invisible characters and double encoding

A UTF-8 BOM (U+FEFF) is not JSON whitespace. Some copy paths and gateways prefix it; JSON.parse then reports an unexpected token at column 0. Zero-width spaces and soft hyphens do the same. Strip with replace(/^\uFEFF/, ""), then trim, before you extract.

Double encoding is quieter. One JSON.stringify yields the string "{\"ok\":true}". Parse that quoted form and you get the string {"ok":true}, not an object. A second parse yields the object. If you stop after one parse and read .ok, you get undefined — it “parsed” and has no fields. Check typeof before you parse again. Do not hard-code “always parse twice”; a real object will throw.

Fix order: change the channel, then extract, repair last

This order beats stacking more prompt sentences:

  1. Change the channel. Final answers go through Structured Output (OpenAI response_format.json_schema, Gemini responseMimeType plus Schema, Claude output_config.format). Tool parameters go through Tool Calling arguments, not scraped prose. See what Structured Output is.
  2. Extract. Strip ```json fences; slice the first balanced value; drop a BOM.
  3. Parse strictly. Use only JSON.parse. On failure, keep the raw text and the error position. Do not eval.
  4. Validate the Schema. A successful parse only means the grammar is legal. Missing fields, wrong types, and extra keys need JSON Schema / ajv. See from prompt to Structured Output.
  5. Repair last. Tools like jsonrepair can close braces and drop trailing commas. Use them only after extract + parse fail, and only if you accept that repairs can change meaning. Then still run steps 3 and 4. Do not make a repairer the global default parser.

Prompts still help: “no fences, no explanation.” They lower the odds of a wrapper. They do not replace a Schema, and they do not loosen JSON.parse. In 2026, treating chat prose as an API means you will keep paying for fences and truncation.

A small extract + parse pipeline

A teaching-sized pipeline: strip fences, drop a BOM, slice a balanced value, then JSON.parse. It handles common wrappers. It does not fix trailing commas or fullwidth punctuation — leave those to Structured Output or an explicit repair layer.

function stripFence(text) {
  const m = String(text).match(/```(?:json|JSON)?\s*([\s\S]*?)```/);
  return m ? m[1] : String(text);
}

function sliceBalancedJson(text) {
  const src = text.replace(/^\uFEFF/, "").trim();
  const start = src.search(/[\{\[]/);
  if (start < 0) throw new SyntaxError("No JSON value found");
  const open = src[start];
  const close = open === "{" ? "}" : "]";
  let depth = 0, inStr = false, esc = false;
  for (let i = start; i < src.length; i++) {
    const ch = src[i];
    if (inStr) {
      if (esc) { esc = false; continue; }
      if (ch === "\\") { esc = true; continue; }
      if (ch === '"') inStr = false;
      continue;
    }
    if (ch === '"') { inStr = true; continue; }
    if (ch === open) depth++;
    else if (ch === close) {
      depth--;
      if (depth === 0) return src.slice(start, i + 1);
    }
  }
  throw new SyntaxError("Unterminated JSON value");
}

function parseModelJson(raw) {
  return JSON.parse(sliceBalancedJson(stripFence(raw)));
}

The slicer must track whether it is inside a string, or a { in a field value closes too early. Nested objects and arrays use depth. If the slice still fails to parse, paste the failed text into the validator and use the class table above. Do not pile more regex onto this layer.

See the error locally

Do not send model output straight to a production parser. In the browser, check three things: is it legal JSON; if not, which column; if you already have a Schema, does it satisfy the contract.

  • JSON validator — see where SyntaxError lands; attach a Schema when you have one.
  • JSON formatter — if it formats, it usually parses; if it fails, look for fullwidth commas or fences in the source.
  • JSON Diff — after a successful parse, compare the model object with the minimal object you allow.

Nothing leaves the browser. That fits a failed model reply, a Schema, and a Tool Calling arguments blob side by side. Stabilize field names and required, then wire the Host.

FAQ

Why does “it looks like JSON” still fail JSON.parse?

Human eyes tolerate fences, trailing commas, curly quotes, and wrapping prose. JSON.parse accepts exactly one RFC 8259 value. Looking like JSON is not the same as being legal JSON.

Is a regex that strips ```json fences enough?

No. Fences are only one wrapper. You still get trailing prose, a second JSON value, trailing commas, and truncation. After stripping fences, slice a balanced value and parse strictly.

How is JSON Mode different from Structured Output?

JSON Mode usually only constrains “looks like JSON,” not fields and types. Structured Output uses JSON Schema to block illegal tokens at decode time. If a program will consume the result, prefer Structured Output. Do not enable JSON Mode and then JSON.parse the chat body.

Should jsonrepair or JSON5 be the default parser?

No. They accept input that should fail, and they can change meaning. Use them only as a repair layer after extract + JSON.parse fail, then still Schema-validate.

When may I call JSON.parse on a streamed response?

After the stream ends and the buffer is a complete value. Parsing a half chunk yields Unexpected end of JSON input every time. To consume tokens as they arrive, use a streaming parser, not JSON.parse.

Parse succeeded but the fields are wrong. Is that this article?

That is the next layer. JSON.parse only guarantees grammar. Missing fields, wrong types, and extra keys are Schema problems — see the Structured Output and Tool Calling validation pieces on this site.

Summary

A JSON.parse failure is a channel problem. Another “please output JSON” sentence will not fix it. Chat models wrap fences, mix dialects, and stop at a token cap. The parser accepts one clean JSON value. In 2026, wire the model through Structured Output or Tool Calling arguments; then extract + strict parse + Schema; repair last.

Prompts can reduce wrappers. They cannot loosen the grammar. Paste the failing text into a local validator, see which column it stops on, then decide: strip a fence, change the channel, or raise the output cap. Models change. What JSON.parse accepts, and what your field contract is, should not.