json.decoder.JSONDecodeError: Invalid Control Character (tested)
JSON strings are not allowed to contain a literal newline, tab, or other control character — they must be escaped as \n, \t, and so on. If one sneaks in unescaped, json.loads() raises Invalid control character at: line X column Y. We measured exactly which characters trigger it and the fix.
1. The error, from a raw newline inside a string value
import json
json.loads('{"a": "line1\nline2"}') # literal newline byte, not an escape sequence
json.decoder.JSONDecodeError: Invalid control character at: line 1 column 13 (char 12)
Measured: this is not "\\n" (backslash-n, the valid two-character escape) — it's an actual newline byte sitting inside the quotes. The JSON spec (RFC 8259) forbids unescaped control characters (anything below U+0020, which includes newline, tab, carriage return, and more obscure ones) inside string literals, full stop.
2. Other control characters trigger the same error
json.loads('{"a": "col1\tcol2"}') # literal tab byte
json.decoder.JSONDecodeError: Invalid control character at: line 1 column 12 (char 11)
json.loads('{"a": "x\x01y"}') # literal 0x01 byte
json.decoder.JSONDecodeError: Invalid control character at: line 1 column 9 (char 8)
Measured: the message and format are identical regardless of which control character it is — tab, a rare byte like 0x01, carriage return, all report the same "Invalid control character at" text with just the position changing. You can't tell which character it was from the message alone; you have to inspect the source bytes at that position yourself.
3. json.dumps() never produces this — it always escapes
json.dumps({"a": "line1\nline2"})
'{"a": "line1\\nline2"}'
json.loads('{"a": "line1\\nline2"}')
{'a': 'line1\nline2'}
Measured: json.dumps() always writes the escaped two-character form \n, never a raw newline byte, so this error cannot come from round-tripping data through Python's own json module. It shows up when the JSON was built by hand (a human editing a config file in a text editor, or multi-line text pasted straight into a string value without escaping), generated by string concatenation instead of json.dumps(), or produced by another language's serializer that doesn't escape correctly.
4. The escape hatch: strict=False
json.loads('{"a": "line1\nline2"}', strict=False)
{'a': 'line1\nline2'}
Measured: passing strict=False to json.loads() accepts the raw control character and includes it as-is in the resulting string — no error, no transformation. This is a deliberate spec violation on the parsing side: per the Python docs, it exists for compatibility with "non-conformant" JSON producers. It's a reasonable one-line fix if you don't control the source and need the data, but the output is then not valid JSON if you write it back out verbatim with something other than json.dumps().
5. How to actually fix it at the source
- If you're generating the JSON yourself: always use
json.dumps()(or your language's equivalent serializer) instead of building the string by hand with f-strings or concatenation — it escapes every control character automatically, so this error becomes structurally impossible on your own output. - If you're consuming JSON from an external source you don't control: try
strict=Falsefirst since it's a one-line change; if the data needs to be re-serialized correctly afterward, run it back throughjson.dumps(), which re-escapes everything properly on the way out. - If it's a hand-edited config file: the usual cause is someone pasting multi-line text into a single-line string field in a text editor — replace the literal line breaks with
\n, or switch the field to a JSON array of lines.
Frequently asked questions
What does "Invalid control character" mean in a JSONDecodeError?
We confirmed it means a literal control-character byte (newline, tab, carriage return, or similar, anything below U+0020) sits unescaped inside a JSON string literal — the JSON spec requires these to be written as escape sequences like \n, not as raw bytes.
Can json.dumps() ever produce output that causes this error?
No — we measured that json.dumps() always writes control characters as their escaped two-character form (\n, \t, etc.), never as a raw byte, so round-tripping data through Python's own json module cannot trigger this; it comes from hand-written or externally-generated JSON.
Does the error message say which control character caused it?
No — we confirmed the message text is identical ("Invalid control character at: line X column Y") regardless of whether it's a newline, tab, or an obscure byte like 0x01; only the position differs, so you have to inspect the source at that offset yourself.
What does strict=False actually do in json.loads()?
We measured that it accepts the raw control character as-is inside the string and returns it successfully, with no error — a deliberate, documented relaxation of the JSON spec for parsing data from non-conformant producers you don't control.
What's the real-world cause of this error in a hand-edited config file?
Usually someone pasted multi-line text into what's meant to be a single-line JSON string value in a text editor, leaving a literal line break in the file instead of an escaped \n — replacing the raw line breaks with \n, or switching to a JSON array of lines, fixes it at the source.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in json module, calling json.loads() on strings containing raw control characters and printing the exact exception text. Every message below is copy-pasted output, not paraphrased. PlainHeaders is our extension; Python's json module is built in.