JSON Lint / Errors

Bad control character in string literal

Your JSON contains a raw control character — usually a newline, tab, or carriage return — sitting unescaped inside a string. JSON strings only allow control characters when written as escape sequences (\n, \t, \r, or \u00XX).

Paste your JSON to find the exact position →

What the error looks like

You'll see one of these in different parsers:

SyntaxError: Bad control character in string literal in JSON at position 42
json.decoder.JSONDecodeError: Invalid control character at: line 3 column 12 (char 42)
JsonReaderException: Bad control character in string literal

Every form points at the same problem: the parser walked into a string and hit a byte it isn't allowed to consume raw.

The four most common causes

1. A literal newline in a "multi-line" string

JSON has no syntax for multi-line strings. This is invalid:

{
  "message": "line one
line two"
}

Fix: join the lines with the escape \n:

{"message": "line one\nline two"}

If you're hand-editing, run the value through JSON.stringify first. If you need real multi-line readability, switch to YAML or store the value as an array of lines.

2. A literal tab from copy-paste

Tabs (U+0009) are invisible. Pasting from a spreadsheet, log file, or CSV often drops them right into your JSON.

{"label": "name	value"}    ← tab between "name" and "value"

Fix: replace tabs with \t, or strip them entirely. Most editors offer "Show invisible characters" to spot them.

3. A Windows-style line ending mid-string

A \r\n pair pasted from a Windows source becomes a raw \r + raw \n inside the string — both forbidden.

Fix: normalize line endings before parsing, or escape both as \r\n.

4. String concatenation instead of serialization

The classic backend bug: building JSON with string templates.

// JS — broken
const body = `{"note": "$${userInput}"}`;

If userInput contains a newline, the result is invalid JSON. Use the language's serializer:

const body = JSON.stringify({ note: userInput });

How to fix programmatically

If you have a string of "almost JSON" and need to repair the embedded control characters, escape them before parsing:

// JavaScript
const safe = raw.replace(/[-]/g, (c) =>
  "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")
);
const data = JSON.parse(safe);

# Python
import re, json
safe = re.sub(r"[\x00-\x1f]", lambda m: f"\\u{ord(m.group()):04x}", raw)
data = json.loads(safe)

This is a workaround, not a fix — the real fix is in the producer. If you don't control it, log a bug upstream while you patch downstream.

Quick reference: which characters break which parsers

CharacterCode pointAllowed raw?Escape
NULU+0000No
TabU+0009No\t
Newline (LF)U+000ANo\n
Carriage returnU+000DNo\r
Form feedU+000CNo\f
BackspaceU+0008No\b
DELU+007FYes (technically)

Defined in RFC 8259 §7.

Related JSON errors

FAQ

What counts as a control character?

Any code point in U+0000–U+001F: NUL, tab (U+0009), newline (U+000A), carriage return (U+000D), and the rest of C0. JSON strings forbid them as raw bytes — they must appear as escape sequences (\t, \n, \r, or \u00XX).

Why does my multi-line string fail?

JSON has no multi-line string literal. A literal newline inside a string is a control character and is invalid. Either join the lines with \n inside a single-line string, or use a format that supports multi-line (YAML, TOML).

I copied this from a database / log file. Why?

Logs and database dumps frequently contain raw tabs and newlines. Wrapping that text in quotes does not escape it. You need to JSON.stringify the string in your producer, not just concatenate it into a JSON template.

Why does Python's json.dumps escape these for me but my hand-written JSON doesn't?

Because json.dumps walks the string and emits the escape sequence for each control character. If you build JSON by string concatenation ("{\"x\":\"" + value + "\"}"), you bypass that and rely on the value already being JSON-safe — which strings rarely are.

What's the difference between this and 'Unexpected token' errors?

Unexpected-token errors fire when the parser sees a structural character it didn't expect (a comma, a brace). Bad-control-character fires specifically when a forbidden byte appears inside a string. Different cause, different fix.

Open the linter →