JSONRepair

Bad Control Character in String Literal: The Invisible Newline That Breaks JSON

The error reads "Bad control character in string literal in JSON at position 26." Position 26 looks empty. That is the whole joke: the character you are looking for is invisible.

The message means exactly one thing. Somewhere inside a quoted string there is a character in the range U+0000 to U+001F, sitting there unescaped. That range is the old ASCII control block: newline, tab, carriage return, and 29 companions you never think about. RFC 8259 says these characters must be escaped inside JSON strings. The parser is not being fussy. It is doing the only thing the spec allows.

The most common source right now is language model output. An agent returns a JSON field containing markdown or code, and buried in it sits a real newline. {"theory": "## Overview followed by an actual line break instead of \n, and the parse dies at the line break. I have seen this exact failure written up in three unrelated open-source projects this year: an agent backend whose tutor responses contained markdown, a streaming tool-call parser choking on tab-indented JSON, and a server audit tool that interpolated shell command output straight into string literals. Different codebases, same invisible character. The shell one is my favorite kind of bug: command substitution strips trailing newlines but keeps interior ones, so a config lookup that returned two lines injected a raw newline right into the middle of the JSON.

How to fix a bad control character in a JSON string literal

Start with what it is not. "Bad escaped character" is the sibling error, and the fix is different. That one complains about a backslash sequence JSON does not define, like \U in "C:\Users\kim". If your message says "bad control character," the backslash is fine and the character itself is the problem. Mix up the two and you will chase the wrong end of the string.

The fixes, in the order I would actually reach for them. First, stop assembling JSON by hand. JSON.stringify in JavaScript and json.dumps in Python escape control characters correctly every time. If you are seeing this error, something upstream is concatenating strings or interpolating values into a template, and the real repair is to replace that with a serializer. Everything below is triage.

Second, repair the text before parsing. The rule is mechanical: inside a string value, every control character becomes an escape. Newline becomes \n, tab becomes \t, carriage return becomes \r, and anything without a short escape becomes \u00XX with its hex code. A sanitizer that does this leaves valid JSON byte-identical, so it is safe to run on every response rather than only the broken ones. In JavaScript it is one line: replace each character in the range U+0000 to U+001F with its \u escape, then hand the result to JSON.parse.

Third, fix the generator. If a model is emitting the JSON, add an explicit instruction to write newlines as \n inside strings. This cuts the error rate a lot. It does not eliminate it, which is why the sanitizer stays in the pipeline as a fallback. Belt and suspenders is the correct posture for machine-generated JSON.

Why the position in the message never helps

The parser reports the position of the control character itself. You go look, and you see a line break. Of course you do. The character is doing exactly what it does. The useful move is to turn on visible whitespace in your editor, or to dump the region with each character's code point. The moment you see 0A sitting naked inside a string, the mystery ends. JSON gets called brittle for rejecting this, and I think that misses the point. A format that silently accepted a raw newline inside a string would hand you a value you never intended, and you would find out three systems later. I will take the loud error.

Paste your broken JSON into the repair tool and it will flag control characters and show you exactly what it escaped.

Frequently asked questions

What counts as a control character in JSON?

Any character from U+0000 to U+001F. In practice you will meet newline (U+000A), carriage return (U+000D), and tab (U+0009). Each must appear as an escape sequence (\n, \r, \t) or as \u00 followed by the hex code. Everything above U+001F is fine as literal text, including emoji.

Why does the reported error position look empty or normal?

Because the offending character is invisible. The parser points at the newline or tab itself. Turn on visible whitespace in your editor, or inspect the character codes around the reported position, and the culprit shows up as a code point instead of a blank.

Is "bad control character" the same as "bad escaped character"?

No. Bad control character means a raw newline, tab, or similar sits unescaped inside a string. Bad escaped character means you wrote a backslash sequence JSON does not define, like \x or \U. Fix the first by escaping the character; fix the second by fixing the backslash.

My JSON came from a language model and fails every other response. What should I do?

Run the escape pass on every response before parsing, not just the failures. Valid JSON passes through unchanged, so there is no downside, and intermittent failures become a non-issue instead of a 2 AM page.

Can I just strip the characters instead of escaping them?

You can, but you lose information: a stripped newline merges two lines into one word. Escaping preserves the content. Strip only when you know the content is disposable, like log prefixes.

Get one practical dev-tools guide a week: subscribe to the newsletter for new JSON guides and parser explainers.

Related reading: Fixing Unescaped Quotes in JSON String Values · How to Fix the JSON Single Quotes Error · The Trailing Comma That Breaks Your JSON · Why Is My JSON Invalid?