Debugging Malformed JSON: A Field Guide
Every JSON parse error has a real cause hiding behind an unhelpful message. A practical guide to the five failure modes that produce 90% of the pain.
SyntaxError: Unexpected token } in JSON at position 4217. You’ve seen this message. Maybe yesterday. Maybe ten minutes ago. It tells you something is wrong, roughly where, and nothing else useful.
This is a practical guide to the small set of failure modes that produce almost all JSON parse errors in the wild, and how to narrow a 10,000-character payload to the exact byte that’s broken.
The five things that usually went wrong
1. A trailing comma
The most common failure mode by a wide margin. You were editing a list or an object, deleted the last element, forgot to delete the comma:
{
"name": "zeroutil",
"category": "developer",
}
JavaScript accepts this. So does JSON5. Plain JSON doesn’t. The parser reports the error at the } because that’s where the grammar breaks — but the actual problem is the comma before it.
How to spot it: search for ,\s*[}\]] with a regex. Our Regex Tester with this pattern will highlight every instance.
How to prevent it: use a formatter as a pre-commit step. JSON generated by JSON.stringify never has trailing commas. Trailing commas are a human-editing artifact.
2. Unquoted or single-quoted keys
{
name: "zeroutil",
'category': "developer"
}
JavaScript object literals accept both, JSON accepts neither. This happens most often when someone types JSON from memory without ever loading it into a parser.
How to spot it: our JSON Formatter will report the exact character position. In a bigger file, grep -E "^\s+[a-zA-Z_]+\s*:" will find unquoted keys.
3. Unescaped characters inside strings
The characters JSON requires you to escape inside strings are: ", \, and the control characters (\n, \t, \r, and so on — anything below U+0020).
{
"sql": "SELECT * FROM users WHERE id = \"42\""
}
is valid. But if you paste in a string that contains an unescaped newline:
{
"bio": "line one
line two"
}
the parser sees the newline as a terminator and crashes. The confusing bit is that the error message usually points at the character after the newline, not at the newline itself.
How to spot it: if the error position doesn’t make obvious sense when you look at the character at that position, check the line above — an unescaped control character terminated a string early.
How to generate safe JSON: always use JSON.stringify or your language’s equivalent to build the output. Don’t concatenate strings into a JSON template; that’s how unescaped quotes end up in your payload.
4. NaN, Infinity, undefined
JavaScript’s JSON.stringify produces null for NaN and Infinity, and omits undefined values entirely. If you see these literals in a “JSON” payload, someone produced it with a non-spec serializer — Python’s json module with allow_nan=True, for example, emits literal NaN, which other language parsers will reject.
{
"accuracy": NaN,
"max_rate": Infinity
}
Valid JavaScript, valid JSON5, invalid JSON.
How to spot it: search for \bNaN\b|\bInfinity\b|\bundefined\b.
How to fix the source: in Python, use json.dumps(obj, allow_nan=False) to get a parse error at serialization time rather than emitting garbage. In JavaScript, either filter before serializing or pass a replacer function to JSON.stringify that converts these values.
5. BOM, stray whitespace, and encoding issues
Some editors and exports add a byte-order mark (U+FEFF) at the start of the file. Most JSON parsers fail on this — the first byte is expected to be { or [, and they don’t accept a UTF-8 BOM as leading whitespace.
<BOM>{"key": "value"}
Looks identical to a valid JSON file until you check the bytes.
How to spot it: file on Unix will report “UTF-8 Unicode (with BOM) text.” head -c 3 file.json | xxd shows the leading bytes. ef bb bf is the BOM.
How to fix: strip it. sed '1s/^\xEF\xBB\xBF//', or save the file as “UTF-8 without BOM” in your editor.
Strategy 1: narrow the position
The parser’s error message gives you a character position. Use it:
try {
JSON.parse(payload);
} catch (err) {
const pos = Number(err.message.match(/position (\d+)/)?.[1]);
console.log(payload.slice(Math.max(0, pos - 80), pos + 80));
console.log(' '.repeat(Math.min(80, pos)) + '^');
}
This prints 80 characters of context around the failing position with a caret pointing at it. In practice, the real problem is usually 1–20 characters to the left of the reported position — the grammar held until it couldn’t.
Strategy 2: binary-bisect a huge payload
If the payload is too big to eyeball, the fastest path to the broken byte is a bisection:
function findBreakingSlice(text) {
let lo = 0, hi = text.length;
while (hi - lo > 1) {
const mid = Math.floor((lo + hi) / 2);
try {
JSON.parse(text.slice(0, mid) + '}]'.repeat(10));
lo = mid;
} catch {
hi = mid;
}
}
return hi;
}
This isn’t perfect (the trailing }] is a cheap way to force valid structure), but it narrows a 10KB file to a 10-byte window in about 10 steps.
Strategy 3: diff against known-good
When the same JSON loaded yesterday and fails today, the fastest thing is a diff against the previous version. Our Diff Checker runs entirely in the browser, which matters if the payload contains anything sensitive — you don’t want to paste a production response into a random diff service.
The diff is most useful on formatted JSON. Run both versions through the JSON Formatter first so structural changes show up cleanly rather than being hidden inside one giant line.
Strategy 4: ask the producer what they were doing
When a payload that comes from an API is malformed, the real bug is usually on the producer side. A specific pattern: the response body got truncated mid-write because a connection was closed, a buffer was flushed at the wrong time, or a timeout fired during serialization. The received text ends in the middle of a string or an object.
If the last byte is not } or ], it’s probably a truncation, not a malformed value. The fix is on the server, not in your parser configuration.
A checklist for “this JSON is broken and I have five minutes”
- Paste it into the formatter. If it complains, the error position is usually within 20 characters of the real problem.
- Grep for
,\s*[}\]]. Trailing commas are the #1 cause. - Check the last character. If it’s not
}or], the stream was truncated. - Check the first three bytes. BOM in hex is
ef bb bf. - Grep for
NaN,Infinity, unescaped quotes ([^\\]").
Five steps, most bugs caught. The rest of the failures — deeply nested structural issues, genuinely weird encoding issues — are rare enough that they deserve the full 30-minute investigation when they show up.
The meta-lesson
JSON parsing has been stable and boring for fifteen years. If your JSON is malformed, almost certainly a human typed it, a broken serializer produced it, or a network issue truncated it. None of those require understanding the spec in depth — they require knowing the common failure modes cold and having tools ready for the binary-search work.
Most of the tools in the wild that claim to “fix” malformed JSON by permissively reinterpreting it are doing you no favors. Fix the source, don’t paper over the parser.
Tools mentioned in this article
- JSON Formatter — Format, validate and minify JSON with syntax highlighting.
- Regex Tester — Test regular expressions with live highlighting, matches and capture groups.
- Diff Checker — Compare code or text with line-by-line diff and unified output.