← Todos los artículos

Carácter de control inválido en literal de cadena JSON: arreglos

Tabuladores, saltos de línea, bytes nulos y códigos de escape ANSI crudos dentro de una cadena JSON disparan este error. Aprende por qué la especificación JSON los prohíbe, cómo se cuelan y cómo eliminarlos o escaparlos.

SyntaxError: Bad control character in string literal in JSON at position N significa que hay un carácter de control crudo y sin escapar —— un tab, una nueva línea, un retorno de carro, o cualquier carácter en el rango U+0000–U+001F —— dentro de un string JSON. La especificación JSON los prohíbe. Este artículo explica por qué, de dónde vienen y cómo deshacerte de ellos.

¿Qué error de string tengo?

¿Qué es un carácter de control?

Los primeros 32 code points de Unicode (U+0000 a U+001F) son caracteres de control —— códigos de formato invisibles heredados de las teletipos. Algunos conocidos:

Code pointNombreEscape JSON
U+0000Null
U+0008Backspace\b
U+0009Tab horizontal\t
U+000ALine feed (nueva línea)\n
U+000CForm feed\f
U+000DCarriage return\r
U+001BEscape (las secuencias ANSI empiezan aquí)

La especificación JSON (RFC 8259 §7) es explícita: todos los caracteres de este rango deben escaparse cuando aparecen dentro de un string JSON. Una nueva línea cruda dentro de un string entre comillas es un error de sintaxis, no un valor.

Por qué las nuevas líneas crudas rompen JSON

Considera un valor de string que contiene un carácter de nueva línea real:

// Cómo se ven los bytes (la nueva línea es literal, no \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

El parser ve la comilla doble de apertura, lee caracteres, y entonces choca con un byte 0x0A crudo. Como un string JSON debe ser una secuencia única e ininterrumpida entre dos comillas dobles en la misma línea lógica, la nueva línea pelada termina el string inesperadamente.

La representación correcta usa la secuencia de escape de dos caracteres \n:

// Correcto —— \n es el escape, no una nueva línea literal
{"message":"line one\nline two"}

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

Cómo entran los caracteres de control al JSON

1. Construyendo strings JSON con template literals o concatenación

const note = `first line
second line`;                   // nueva línea real desde el template literal

const json = `{"note":"${note}"}`; // nueva línea cruda embebida en el string
JSON.parse(json);               // SyntaxError: Bad control character…

// ✓ Solución: usa siempre JSON.stringify para los valores, nunca interpoles a mano
const json = JSON.stringify({ note });  // escapa la nueva línea automáticamente

2. Leyendo archivos e incrustando contenido tal cual

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

// ❌ Construcción manual de string —— incrusta caracteres de control crudos
const json = '{"content":"' + content + '"}';

// ✓ JSON.stringify maneja todo el escape
const json = JSON.stringify({ content });

3. Respuestas de API de sistemas que no escapan su salida

Algunos sistemas back-end (scripts PHP legacy, serializadores propios, triggers de base de datos que construyen JSON a mano) emiten valores de campo con nuevas líneas o tabs sin escapar. Recibes JSON sintácticamente inválido y response.json() lanza.

Diagnostícalo logueando la longitud cruda del body y los primeros 200 caracteres. Busca saltos de línea visibles dentro de lo que debería ser un valor de string.

4. Pegar desde un terminal (secuencias de escape ANSI)

La salida del terminal contiene códigos de color ANSI que empiezan con el carácter Escape (0x1B) seguido de [. Si pegas salida de terminal en un string JSON, cada reset de color (\x1B[0m) se vuelve un carácter de control.

// Salida del terminal pegada en un string JSON:
{"log":"\u001B[32mOK\u001B[0m request processed"}
//      ^^^ byte ESC crudo —— debería ser la secuencia de escape \u001B de 6 caracteres

5. Bytes nulos de datos binarios

Leer parte de un archivo binario, una columna BLOB de base de datos, o un string estilo C dentro de un campo JSON puede embeber bytes nulos (U+0000), que son la fuente más común de este error en pipelines de DB a JSON.

Cómo arreglarlo

Prevención: usa siempre JSON.stringify

Esta es la solución definitiva. JSON.stringify() escapa correctamente cada carácter de control —— no necesitas escapar a mano nunca.

// ✓ Seguro sin importar qué contenga userInput
const json = JSON.stringify({ message: userInput });

Reparación: elimina o escapa después

Si estás recibiendo JSON roto de una fuente externa y no puedes arreglar el productor, puedes sanear el string crudo antes de parsear. El enfoque más seguro es escapar los caracteres de control pelados:

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);

Si los caracteres de control son ruido (códigos ANSI, bytes nulos) en vez de datos con significado, eliminarlos es más simple:

// Elimina todos los caracteres de control excepto \t, \n, \r (comunes en texto)
const cleaned = raw.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '');
const data = JSON.parse(cleaned);

Dos fuentes más del mundo real

  • BOM UTF-8 en posición 0. Los archivos guardados como «UTF-8 con BOM» empiezan con 0xEF 0xBB 0xBF. RFC 8259 prohíbe BOM en JSON, y JSON.parse lo rechaza con un error en posición 0 que es fácil confundir con un carácter de control. Elimínalo con text.replace(/^/, '') antes de parsear, o guarda el archivo como «UTF-8» (sin BOM) en tu editor.
  • VS Code: «Remove Control Characters». Si estás limpiando un solo archivo a mano, el Selection → Remove All Occurrences of Find Match de VS Code combinado con find regex sobre [\x00-\x08\x0b\x0c\x0e-\x1f] (preservando los reales \t\n\r) borra los bytes de control invisibles de una pasada. También hay un comando «Remove Control Characters» en algunas extensiones si haces esto a menudo.

Detectando la posición ofensiva

El mensaje de error incluye una posición en bytes. Úsala para inspeccionar qué hay ahí:

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 reescapa los caracteres de control para que los veas
    // p. ej. "...line one\nline two..." —— ese \n era una nueva línea cruda
  }
}

Preguntas frecuentes

¿Qué es un bad control character en JSON?

Un carácter crudo y sin escapar en el rango U+0000–U+001F —— un tab, nueva línea, retorno de carro, byte nulo, o escape ANSI —— dentro de un string JSON. RFC 8259 §7 exige que cada uno de ellos esté escapado (por ejemplo \n para una nueva línea).

¿Cómo arreglo un error de bad control character?

El arreglo definitivo es construir el JSON con JSON.stringify(), que escapa caracteres de control automáticamente. Si recibes JSON roto que no puedes arreglar en la fuente, escapa o elimina los caracteres de control con una regex antes de parsear (ver snippets de arriba).

¿Por qué una nueva línea cruda rompe JSON?

Un string JSON debe ser una secuencia ininterrumpida entre dos comillas dobles. Un byte de nueva línea literal (0x0A) termina el string antes de tiempo, así que el parser reporta un error de carácter de control. La representación válida es el escape de dos caracteres \n.

¿Cómo elimino códigos de color ANSI de JSON?

Las secuencias ANSI empiezan con el byte Escape (U+001B). Elimínalas con raw.replace(/\[[0-9;]*m/g, '') antes de parsear, o evita pegar salida cruda de terminal en valores de string JSON desde el principio.

Repara JSON roto en tu navegador

Pega tu JSON roto en JSON Fix en fixjson.org. El parser indulgente identifica violaciones de caracteres de control y reporta la posición exacta. Para casos simples puede reparar el string automáticamente.