JSON1

Sintaxis

Escapado en JSON

Copiaste algo de un archivo de log y es ilegible: cada comilla lleva una barra invertida delante y las llaves están dentro de comillas. Nada está roto. Estás viendo un documento JSON que se guardó como una cadena JSON, y hay una forma mecánica de volver.

Para qué sirve el escapado

Una cadena JSON se delimita con comillas dobles. Así que en cuanto el texto interior necesita una comilla doble propia, hay un conflicto: el parser leería esa comilla como el fin de la cadena. El escapado lo resuelve poniendo una barra invertida delante — \" significa un carácter de comilla literal, no un delimitador.

El mismo truco cubre todo lo demás que no puede aparecer en crudo dentro de una cadena: la propia barra invertida y los caracteres de control por debajo de U+0020.

El conjunto completo de escapes que define JSON. No hay otros.
EscapeSignifica
\"Una comilla doble, no el fin de la cadena
\\Una barra invertida literal
\nSalto de línea
\rRetorno de carro
\tTabulación
\b \fRetroceso y salto de página
\/Una barra normal. Legal, pero nunca obligatoria
\uXXXXCualquier carácter por punto de código

Qué no necesita escaparse

Vale la pena decirlo porque escapar de más es tan común como escapar de menos. El texto no ASCII no necesita escaparse en absoluto. JSON es Unicode, así que las letras acentuadas, los caracteres chinos y los emoji son válidos en crudo dentro de una cadena.

  • Las barras normales nunca lo necesitan. https://example.com está bien tal cual. \/ es legal y algunos codificadores lo emiten, y por eso las URLs en los logs a menudo se ven raras sin motivo.
  • Las comillas simples nunca lo necesitan. No tienen significado especial en JSON, así que \' no es un escape válido: es un error.
  • Los caracteres de control siempre lo necesitan. Una tabulación o un salto de línea en crudo dentro de una cadena es JSON inválido. Se vuelven \t y \n; algo más raro se vuelve del estilo \u0001.

JSON de entrada

{"s":"café 中文 😀"}

Tras escapar

"{\"s\":\"café 中文 😀\"}"
Solo las comillas estructurales ganaron una barra invertida. café, 中文 y el emoji pasaron intactos — \u00e9 también habría sido legal, solo ilegible.

Caracteres de control

Una tabulación y un salto de línea dentro de un valor de cadena no pueden escribirse literalmente, así que salen como escapes de dos caracteres. Es el único caso donde el escapado cambia el tamaño de forma que la gente lo nota: una cadena multilínea se alarga visiblemente.

JSON de entrada

{"s":"a\tb\r\nc"}

Tras escapar

"{\"s\":\"a\\tb\\r\\nc\"}"
El \t de la entrada ya era un escape. Tras una ronda es \\t — una barra invertida seguida de una t, que es cómo una capa anidada lo preserva.

Por qué se multiplican las barras

Aquí está la causa real del desastre. Un servicio registra un cuerpo de petición metiendo el documento JSON entero en un campo de cadena de otro documento JSON. Las comillas del documento interior quedan ahora dentro de una cadena, así que cada una recibe una barra invertida.

Los datos

{"user":{"name":"Ada"},"ok":true}

Guardados como cadena JSON

"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"
Las llaves están ahora dentro de comillas: el documento entero se ha vuelto un único valor de cadena.

Dos capas, y la duplicación

Hazlo dos veces — un servicio registra un mensaje que ya contenía datos registrados — y cada barra invertida de la primera ronda necesita ahora escaparse a su vez. Una comilla se vuelve \\\": una barra invertida escapada seguida de una comilla escapada.

Por eso el número parece arbitrario. No lo es: cada capa aproximadamente duplica las barras, así que 1, 3 o 7 barras significan una, dos o tres capas de codificación.

Una capa

"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"

Dos capas

"\"{\\\"user\\\":{\\\"name\\\":\\\"Ada\\\"},\\\"ok\\\":true}\""
Contar las barras antes de una comilla te dice cuántas veces desescapar. Tres significa dos veces.

Recuperar los datos

Desescapar es la inversa, una capa a la vez. Introduce la cadena escapada y recuperas el documento, ya formateado:

  • Una pulsación quita una capa. Una cadena doblemente codificada vuelve como una cadena aún escapada, no como el documento. Púlsalo otra vez.
  • El botón está deshabilitado cuando no hay nada que quitar. Solo se activa cuando la entrada se parsea como una cadena que a su vez se parsea como JSON, así que un clic accidental no puede estropear un documento normal.
  • Si pegas datos escapados, el formateador lo detecta. Lo dice en lugar de reformatear en silencio, porque una cadena que casualmente contiene JSON y un documento JSON son cosas distintas.

Entrada escapada

"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"

Tras quitar los escapes

{
  "user": {
    "name": "Ada"
  },
  "ok": true
}
Salida real del botón Quitar escapes del formateador.

Evitarlo desde el principio

La codificación anidada casi siempre es accidental. Si controlas el código que la produce, estas son las soluciones, ordenadas por cuánto ayudan.

  • Registra estructurado, no serializado. La mayoría de los loggers aceptan un objeto para un campo. logger.info({ body: payload }) anida correctamente; JSON.stringify(payload) crea la capa que luego tienes que pelar.
  • No serialices antes de guardar en una columna JSON. El jsonb de Postgres y equivalentes toman el valor directamente. Serializar primero guarda una cadena, y cada consulta contra ella necesita un cast.
  • Nunca construyas JSON concatenando cadenas. De ahí viene el escapado insuficiente: una comilla en el apellido de alguien termina la cadena antes de tiempo y todo el payload se vuelve inválido. Serializa con una librería.
  • Revisa los payloads de las colas de mensajes. Un cuerpo que ya es JSON no necesita recodificarse al entrar en una cola que transporta texto.