← Alle Artikel

Schlechtes Steuerzeichen in String-Literal in JSON: Fixes

Rohe Tabs, Zeilenumbrüche, Nullbytes und ANSI-Escape-Codes in einem JSON-String lösen diesen Fehler aus. Lerne, warum die JSON-Spec sie verbietet, wie sie sich einschleichen und wie du sie entfernst oder escapest.

SyntaxError: Bad control character in string literal in JSON at position N bedeutet, dass ein rohes, nicht escaptes Steuerzeichen —— ein Tab, Zeilenumbruch, Carriage Return, oder irgendein Zeichen im Bereich U+0000–U+001F —— innerhalb eines JSON-Strings sitzt. Die JSON-Spezifikation verbietet sie. Dieser Artikel erklärt warum, woher sie kommen, und wie du sie loswirst.

Welcher String-Fehler trifft mich?

Was ist ein Steuerzeichen?

Die ersten 32 Unicode-Codepoints (U+0000 bis U+001F) sind Steuerzeichen —— unsichtbare Formatierungscodes, geerbt von Fernschreibern. Einige bekannte:

CodepointNameJSON-Escape
U+0000Null
U+0008Backspace\b
U+0009Horizontaler Tab\t
U+000ALine Feed (Zeilenumbruch)\n
U+000CForm Feed\f
U+000DCarriage Return\r
U+001BEscape (ANSI-Sequenzen beginnen hier)

Die JSON-Spezifikation (RFC 8259 §7) ist explizit: alle Zeichen in diesem Bereich müssen escapt werden, wenn sie innerhalb eines JSON-Strings vorkommen. Ein roher Zeilenumbruch in einem gequoteten String ist ein Syntaxfehler, kein Wert.

Warum rohe Zeilenumbrüche JSON brechen

Stell dir einen String-Wert vor, der ein echtes Zeilenumbruch-Zeichen enthält:

// Wie die Bytes aussehen (der Zeilenumbruch ist literal, nicht \n):
{"message":"line one
line two"}

JSON.parse('{"message":"line one\nline two"}')
// SyntaxError: Bad control character in string literal in JSON at position 20

Der Parser sieht das öffnende doppelte Anführungszeichen, liest Zeichen, dann trifft er auf ein rohes 0x0A-Byte. Da ein JSON-String eine einzelne, ununterbrochene Folge zwischen zwei doppelten Anführungszeichen auf derselben logischen Zeile sein muss, beendet der nackte Zeilenumbruch den String unerwartet.

Die korrekte Darstellung nutzt die Zwei-Zeichen-Escape-Sequenz \n:

// Korrekt —— \n ist der Escape, kein literaler Zeilenumbruch
{"message":"line one\nline two"}

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

Wie Steuerzeichen ins JSON gelangen

1. JSON-Strings mit Template Literals oder Konkatenation bauen

const note = `first line
second line`;                   // echter Zeilenumbruch aus dem Template Literal

const json = `{"note":"${note}"}`; // roher Zeilenumbruch im String eingebettet
JSON.parse(json);               // SyntaxError: Bad control character…

// ✓ Fix: nutze immer JSON.stringify für Werte, niemals manuell interpolieren
const json = JSON.stringify({ note });  // escapt den Zeilenumbruch automatisch

2. Dateien lesen und Inhalt wortwörtlich einbetten

import fs from 'fs';
const content = fs.readFileSync('notes.txt', 'utf8');
// content kann \n, \r\n, \t etc. enthalten

// ❌ Manuelles String-Bauen —— bettet rohe Steuerzeichen ein
const json = '{"content":"' + content + '"}';

// ✓ JSON.stringify behandelt jegliches Escaping
const json = JSON.stringify({ content });

3. API-Antworten von Systemen, die Output nicht escapen

Manche Backend-Systeme (Legacy-PHP-Skripte, eigene Serialisierer, Datenbank-Trigger, die JSON manuell bauen) liefern Feldwerte mit unescapten Zeilenumbrüchen oder Tabs aus. Du bekommst syntaktisch ungültiges JSON, und response.json() wirft.

Diagnostiziere, indem du die rohe Body-Länge und die ersten 200 Zeichen loggst. Achte auf sichtbare Zeilenumbrüche in dem, was eigentlich ein String-Wert sein sollte.

4. Aus einem Terminal kopieren (ANSI-Escape-Sequenzen)

Terminal-Output enthält ANSI-Farbcodes, die mit dem Escape-Zeichen (0x1B) beginnen, gefolgt von [. Wenn du Terminal-Output in einen JSON-String einfügst, wird jeder Farb-Reset (\x1B[0m) zu einem Steuerzeichen.

// In einen JSON-String eingefügter Terminal-Output:
{"log":"\u001B[32mOK\u001B[0m request processed"}
//      ^^^ rohes ESC-Byte —— sollte die 6-Zeichen-Sequenz \u001B sein

5. Null-Bytes aus Binärdaten

Wenn du Teile einer Binärdatei, eine Datenbank-BLOB-Spalte oder einen C-artigen String in ein JSON-Feld einliest, kannst du Null-Bytes (U+0000) einbetten —— die häufigste Quelle dieses Fehlers in DB-zu-JSON-Pipelines.

Wie du es behebst

Prävention: nutze immer JSON.stringify

Das ist die endgültige Lösung. JSON.stringify() escapt jedes Steuerzeichen korrekt —— du musst nie manuell escapen.

// ✓ Sicher, egal was userInput enthält
const json = JSON.stringify({ message: userInput });

Reparatur: nachträglich strippen oder escapen

Wenn du kaputtes JSON aus einer externen Quelle bekommst und den Producer nicht reparieren kannst, kannst du den rohen String vor dem Parsen säubern. Der sicherste Ansatz ist, nackte Steuerzeichen zu escapen:

function escapeControlChars(raw) {
  return raw.replace(
    /[\u0000-\u001F]/g,
    (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')
  );
}

const fixed = escapeControlChars(rawFromApi);
const data  = JSON.parse(fixed);

Wenn die Steuerzeichen Rauschen sind (ANSI-Codes, Null-Bytes) statt sinnvolle Daten, ist Strippen einfacher:

// Strippe alle Steuerzeichen außer \t, \n, \r (die in Text üblich sind)
const cleaned = raw.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '');
const data = JSON.parse(cleaned);

Zwei weitere reale Quellen

  • UTF-8-BOM an Position 0. Als „UTF-8 mit BOM" gespeicherte Dateien beginnen mit 0xEF 0xBB 0xBF. RFC 8259 verbietet ein BOM in JSON, und JSON.parse lehnt es mit einem Position-0-Fehler ab, der leicht als Steuerzeichen-Fehler missgelesen wird. Strippe vor dem Parsen mit text.replace(/^/, ''), oder speichere die Datei als „UTF-8" (ohne BOM) in deinem Editor.
  • VS Code: „Remove Control Characters". Wenn du eine einzelne Datei manuell aufräumst, löscht VS Codes Selection → Remove All Occurrences of Find Match kombiniert mit Regex-Suche nach [\x00-\x08\x0b\x0c\x0e-\x1f] (echte \t\n\r bleiben erhalten) unsichtbare Steuerbytes in einem Durchgang. Es gibt auch in manchen Erweiterungen einen „Remove Control Characters"-Befehl, wenn du das oft tust.

Die Fehlerstelle erkennen

Die Fehlermeldung enthält eine Byte-Position. Nutze sie, um zu prüfen, was dort steht:

try {
  JSON.parse(raw);
} catch (e) {
  const pos = Number(e.message.match(/position (\d+)/)?.[1]);
  if (!isNaN(pos)) {
    const context = raw.slice(Math.max(0, pos - 20), pos + 20);
    console.log('Context around error:', JSON.stringify(context));
    // JSON.stringify re-escapt die Steuerzeichen, damit du sie sehen kannst
    // z. B. "...line one\nline two..." —— das \n war ein roher Zeilenumbruch
  }
}

Häufig gestellte Fragen

Was ist ein bad control character in JSON?

Ein rohes, nicht escaptes Zeichen im Bereich U+0000–U+001F —— ein Tab, Zeilenumbruch, Carriage Return, Null-Byte oder ANSI-Escape —— innerhalb eines JSON-Strings. RFC 8259 §7 verlangt, dass jedes solche Zeichen escapt wird (z. B. \n für einen Zeilenumbruch).

Wie behebe ich einen bad-control-character-Fehler?

Der endgültige Fix ist, das JSON mit JSON.stringify() zu bauen, das Steuerzeichen automatisch escapt. Wenn du kaputtes JSON empfängst, das du an der Quelle nicht reparieren kannst, escape oder strippe die Steuerzeichen mit einer Regex vor dem Parsen (siehe Snippets oben).

Warum bricht ein roher Zeilenumbruch JSON?

Ein JSON-String muss eine ununterbrochene Folge zwischen zwei doppelten Anführungszeichen sein. Ein literales Newline-Byte (0x0A) beendet den String vorzeitig, also meldet der Parser einen Steuerzeichen-Fehler. Die gültige Darstellung ist der Zwei-Zeichen-Escape \n.

Wie entferne ich ANSI-Farbcodes aus JSON?

ANSI-Sequenzen beginnen mit dem Escape-Byte (U+001B). Strippe sie vor dem Parsen mit raw.replace(/\[[0-9;]*m/g, ''), oder vermeide es von vornherein, rohen Terminal-Output in JSON-String-Werte zu kleben.

Kaputtes JSON im Browser reparieren

Füge dein kaputtes JSON in JSON Fix auf fixjson.org ein. Der nachsichtige Parser identifiziert Steuerzeichen-Verstöße und meldet die exakte Position. Für einfache Fälle kann er den String automatisch reparieren.