← All articles

Bad Control Character in JSON: Fix Raw Newlines, Tabs, NULs, and ANSI Bytes

Fix bad control character JSON errors by finding raw tabs, newlines, carriage returns, NUL bytes, and ANSI escape bytes inside strings, then escaping or removing them safely.

SyntaxError: Bad control character in string literal in JSON at position N means a raw control character appears inside a quoted JSON string. The usual culprits are a literal newline, tab, carriage return, NUL byte, or ANSI escape byte.

Valid JSON can contain those characters as escaped sequences:

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

Invalid JSON contains the actual byte inside the string:

{"message":"line one<RAW LF>line two"}

That difference is easy to miss in a log viewer because both may look like a line break. The parser does not care how it looks on screen. It cares whether the JSON text contains the two characters \ and n, or a literal 0x0A line-feed byte inside the string.

Quick Fix

If you are creating the JSON, stop building it by hand. Put the real value in an object and let the runtime escape it:

const message = `line one
line two`;

const json = JSON.stringify({ message });
console.log(json);
// {"message":"line one\nline two"}

In Python:

import json

message = "line one\nline two"
payload = json.dumps({"message": message})
print(payload)
# {"message": "line one\nline two"}

If you are receiving broken JSON from another system, first identify the control character and where it sits. Then either:

  • ask the producer to emit valid JSON,
  • escape the raw character inside the string, or
  • remove it only when it is noise, such as ANSI color codes or NUL bytes from binary data.

Avoid a blind global replacement of every control character. Pretty-printed JSON uses real newlines and spaces between tokens, and those structural newlines are legal. The problem is a raw control character inside a string value.

What Counts as a Control Character?

JSON strings cannot contain unescaped characters from U+0000 through U+001F. These are the C0 control characters.

Code point Common name Valid JSON representation inside a string
U+0000 NUL \u0000
U+0008 Backspace \b
U+0009 Horizontal tab \t
U+000A Line feed \n
U+000C Form feed \f
U+000D Carriage return \r
U+001B Escape, often used by ANSI color codes \u001b

The JSON rule is narrow but strict: these characters must be escaped when they appear inside a JSON string. Outside strings, whitespace such as newline, carriage return, tab, and space is allowed between JSON tokens.

That is why this is valid:

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

And this is not:

{
  "message": "line one<RAW LF>line two"
}

Bad Control Character vs Bad Escaped Character

These two errors sound similar, but they point to different bytes.

Error family What is wrong Example
Bad control character A raw control byte is inside the string. line one<RAW LF>line two
Bad escaped character A backslash is followed by a character JSON does not allow. C:\Users\Ada because \U is not a JSON escape
Unterminated string A string starts with " but never closes. {"name":"Ada}

Use Bad Escaped Character in JSON when the problem is a backslash sequence such as \x, \U, or an unescaped Windows path. Use Unterminated String in JSON when the quote is missing. This page is for raw invisible bytes inside a string.

On fixjson.org, the strict parser reports this as Unescaped control character in string and maps the error to this guide. Browser engines may phrase the same problem as Bad control character in string literal in JSON at position N.

The JavaScript Trap: One Backslash Is Not Enough

This example looks like it contains valid JSON:

JSON.parse('{"message":"line one\nline two"}');

But it fails. JavaScript processes \n before JSON.parse() sees the string. The parser receives a JSON text containing a real newline inside "message".

If you must write JSON text inside a JavaScript string literal, escape the backslash too:

JSON.parse('{"message":"line one\\nline two"}');

In real code, prefer JSON.stringify() instead of hand-writing the JSON text:

const message = 'line one\nline two';
const json = JSON.stringify({ message });
const data = JSON.parse(json);

That round trip keeps the newline as data while producing valid JSON text.

Where Raw Control Characters Come From

Manual JSON concatenation

This is the most common cause:

const note = `first line
second line`;

const json = '{"note":"' + note + '"}';
JSON.parse(json);
// SyntaxError: Bad control character in string literal in JSON at position ...

The value is fine. The JSON construction is not. Build an object and stringify it:

const json = JSON.stringify({ note });

Log files and terminal output

Terminal output can contain ANSI color sequences. The escape byte is U+001B, often shown as ESC or written in source as \x1b.

If colored terminal text is pasted into a JSON string, the raw escape byte can break parsing:

{"log":"<ESC>[32mOK<ESC>[0m request processed"}

If the color codes are noise, remove them before building JSON:

const withoutAnsi = terminalText.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '');
const json = JSON.stringify({ log: withoutAnsi });

Binary data or NUL-padded database fields

NUL bytes (U+0000) often appear when binary data, fixed-width database fields, or C-style strings leak into a text pipeline.

If the byte is not meaningful text, strip it before serialization:

const clean = value.replace(/\u0000/g, '');
const json = JSON.stringify({ value: clean });

If the byte is meaningful data, do not put raw binary into JSON as a string. Encode it first, usually as Base64, and document that contract.

Files embedded as JSON fields

Reading a text file and placing its content in a JSON field is safe only if the JSON serializer does the escaping:

import fs from 'node:fs';

const content = fs.readFileSync('notes.txt', 'utf8');
const json = JSON.stringify({ content });

This is unsafe:

const json = '{"content":"' + content + '"}';

It will break as soon as the file contains a real newline, tab, quote, or backslash that is not escaped for JSON.

API producers that hand-roll JSON

Legacy scripts, database string builders, template engines, and custom serializers sometimes return almost-JSON. The payload may look reasonable until one record contains a newline in a comment field.

When response.json() fails, capture the raw text first:

const response = await fetch('/api/orders');
const raw = await response.text();

try {
  const data = JSON.parse(raw);
} catch (error) {
  console.error(error.message);
  console.error(JSON.stringify(raw.slice(0, 300)));
}

JSON.stringify() around the snippet is intentional. It makes invisible characters visible as escapes in your debug output.

Find the Offending Character

Browser and Node errors often include a character position. Use that position to inspect the exact code point:

function inspectPosition(text, position) {
  const ch = text[position];
  return {
    position,
    codePoint: `U+${ch.charCodeAt(0).toString(16).padStart(4, '0').toUpperCase()}`,
    characterAsJson: JSON.stringify(ch),
    context: JSON.stringify(text.slice(Math.max(0, position - 25), position + 25)),
  };
}

try {
  JSON.parse(raw);
} catch (error) {
  const match = String(error.message).match(/position (\d+)/);
  if (match) {
    console.log(inspectPosition(raw, Number(match[1])));
  }
}

For files, scan for unusual control characters that are rarely valid even outside strings, such as NUL and ESC:

python3 - <<'PY'
from pathlib import Path

text = Path("payload.json").read_text(errors="replace")

for index, char in enumerate(text):
    code = ord(char)
    if code < 0x20 and char not in "\t\n\r":
        print(index, f"U+{code:04X}", repr(char))
PY

This scan intentionally ignores tab, newline, and carriage return because those are legal whitespace between JSON tokens. To prove they are inside a string, use a parser error location or a string-aware repair step.

Repair Broken JSON Safely

The best repair is at the producer. If a service emits invalid JSON, fix the serializer there. Consumers should not have to guess whether a newline was content, a delimiter, or a copy-paste accident.

When you cannot fix the producer, repair only the narrow problem. This function walks the JSON text, tracks whether it is currently inside a string, and escapes raw control characters only inside strings:

function escapeControlCharsInsideStrings(text) {
  let output = '';
  let inString = false;
  let escaped = false;

  for (const ch of text) {
    const code = ch.charCodeAt(0);

    if (!inString) {
      output += ch;
      if (ch === '"') inString = true;
      continue;
    }

    if (escaped) {
      output += ch;
      escaped = false;
      continue;
    }

    if (ch === '\\') {
      output += ch;
      escaped = true;
      continue;
    }

    if (ch === '"') {
      output += ch;
      inString = false;
      continue;
    }

    if (ch === '\b') output += '\\b';
    else if (ch === '\t') output += '\\t';
    else if (ch === '\n') output += '\\n';
    else if (ch === '\f') output += '\\f';
    else if (ch === '\r') output += '\\r';
    else if (code < 0x20) output += `\\u${code.toString(16).padStart(4, '0')}`;
    else output += ch;
  }

  return output;
}

const repaired = escapeControlCharsInsideStrings(raw);
const data = JSON.parse(repaired);

This is safer than replacing every [\u0000-\u001f] in the whole document. A global replacement can corrupt legal formatting whitespace outside strings.

If a control character is definitely noise, strip that specific class before parsing:

const withoutNuls = raw.replace(/\u0000/g, '');
const withoutAnsi = withoutNuls.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '');
const data = JSON.parse(withoutAnsi);

Do not strip all tabs and newlines from a JSON file unless you know they are never part of intended string content.

Validate After Repair

After any repair, validate the result with a strict parser:

const repaired = escapeControlCharsInsideStrings(raw);
const value = JSON.parse(repaired);
console.log(JSON.stringify(value, null, 2));

On the command line:

jq empty repaired.json
python3 -m json.tool repaired.json > /dev/null

Those commands confirm JSON syntax. They do not prove the payload matches your API contract. If you need required fields, types, enum values, or numeric ranges, validate the parsed value with JSON Schema.

What About UTF-8 BOM?

A UTF-8 byte order mark is not a C0 control character. It is U+FEFF, and it usually appears at the start of a file. It can still create a confusing position-0 parse failure.

If you see an invisible character before the opening { or [, strip the BOM separately:

const withoutBom = raw.replace(/^\uFEFF/, '');
const data = JSON.parse(withoutBom);

Do not treat BOM cleanup as the same thing as repairing raw newlines inside strings. They are different problems.

Use JSON Fix for a One-Off Payload

Paste the broken payload into JSON Fix when you need a quick local repair. The repair parser can identify unescaped control characters and, for simple cases, remove or normalize them so the output becomes strict JSON.

For a strict yes/no check with line and column feedback, use JSON Validator. The validator does not enforce JSON Schema; it tells you whether the text parses as strict JSON.

Frequently Asked Questions

What is a bad control character in JSON?

It is a raw, unescaped character from U+0000 through U+001F inside a JSON string. Common examples are literal tabs, newlines, carriage returns, NUL bytes, and ANSI escape bytes.

How do I fix a bad control character error?

If you create the JSON, use JSON.stringify() in JavaScript or json.dumps() in Python so the serializer escapes control characters. If you receive broken JSON, find the exact character, then escape it inside the string or remove it only if it is noise.

Why does a raw newline break JSON?

A JSON string cannot contain a literal line-feed byte. It must contain the escaped sequence \n. In JavaScript source, that means JSON text inside a string literal often needs \\n so JSON.parse() receives the backslash.

Is a tab allowed in JSON?

A tab is allowed as whitespace between JSON tokens. A raw tab is not allowed inside a JSON string. Inside a string, use \t.

Is \u0000 valid JSON?

Yes. \u0000 is an escaped representation of the NUL character, so it is valid inside a JSON string. A raw NUL byte inside the string is not valid JSON.

Should I strip all control characters before parsing JSON?

Usually no. Newlines, carriage returns, and tabs are legal outside strings and may be meaningful inside string values after escaping. Strip only known noise such as NUL padding or ANSI color codes, or use a string-aware repair step.

How do I remove ANSI color codes from JSON input?

Remove ANSI sequences before serialization, for example with text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, ''), then build JSON with JSON.stringify(). If the JSON text is already broken, repair or remove the raw ESC bytes before parsing.

Is this the same as a bad escaped character?

No. A bad control character is a raw byte inside a string. A bad escaped character is an invalid sequence after a backslash, such as \x or \U. The fixes are different.

Related Guides

Sources

Last reviewed July 2026.