JSON1

Sintaxe

Escape em JSON

Você copiou algo de um arquivo de log e está ilegível: cada aspa tem uma barra invertida na frente e as chaves estão dentro de aspas. Nada está quebrado. Você está vendo um documento JSON que foi guardado como uma string JSON, e existe um caminho mecânico de volta.

Para que serve o escape

Uma string JSON é delimitada por aspas duplas. Então no momento em que o texto interno precisa de uma aspa dupla própria, há um conflito: o parser leria essa aspa como o fim da string. O escape resolve colocando uma barra invertida na frente — \" significa um caractere de aspa literal, não um delimitador.

O mesmo truque cobre todo o resto que não pode aparecer cru dentro de uma string: a própria barra invertida e os caracteres de controle abaixo de U+0020.

O conjunto completo de escapes que o JSON define. Não há outros.
EscapeSignifica
\"Uma aspa dupla, não o fim da string
\\Uma barra invertida literal
\nNova linha
\rRetorno de carro
\tTabulação
\b \fBackspace e form feed
\/Uma barra normal. Legal, mas nunca obrigatória
\uXXXXQualquer caractere por ponto de código

O que não precisa de escape

Vale dizer porque escapar demais é tão comum quanto escapar de menos. Texto não ASCII não precisa de escape nenhum. JSON é Unicode, então letras acentuadas, caracteres chineses e emoji são todos válidos crus dentro de uma string.

  • Barras normais nunca precisam. https://example.com está bem como está. \/ é legal e alguns codificadores emitem, e é por isso que URLs em logs muitas vezes ficam estranhas sem motivo.
  • Aspas simples nunca precisam. Não têm significado especial em JSON, então \' não é um escape válido: é um erro.
  • Caracteres de controle sempre precisam. Uma tabulação ou nova linha crua dentro de uma string é JSON inválido. Viram \t e \n; algo mais obscuro vira do tipo \u0001.

JSON de entrada

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

Depois do escape

"{\"s\":\"café 中文 😀\"}"
Só as aspas estruturais ganharam barra invertida. café, 中文 e o emoji passaram intactos — \u00e9 também seria legal, só ilegível.

Caracteres de controle

Uma tabulação e uma nova linha dentro de um valor de string não podem ser escritas literalmente, então saem como escapes de dois caracteres. É o único caso em que o escape muda o tamanho de um jeito que as pessoas notam: uma string multilinha fica visivelmente mais longa.

JSON de entrada

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

Depois do escape

"{\"s\":\"a\\tb\\r\\nc\"}"
O \t da entrada já era um escape. Depois de uma rodada é \\t — uma barra invertida seguida de um t, que é como uma camada aninhada o preserva.

Por que as barras se multiplicam

Aqui está a causa real da confusão. Um serviço registra um corpo de requisição colocando o documento JSON inteiro em um campo de string de outro documento JSON. As aspas do documento interno agora ficam dentro de uma string, então cada uma recebe uma barra invertida.

Os dados

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

Guardados como string JSON

"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"
As chaves agora estão dentro de aspas: o documento inteiro virou um único valor de string.

Duas camadas, e a duplicação

Faça duas vezes — um serviço registra uma mensagem que já continha dados registrados — e cada barra invertida da primeira rodada agora precisa de escape ela mesma. Uma aspa vira \\\": uma barra invertida escapada seguida de uma aspa escapada.

É por isso que a contagem parece arbitrária. Não é: cada camada aproximadamente dobra as barras, então 1, 3 ou 7 barras significam uma, duas ou três camadas de codificação.

Uma camada

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

Duas camadas

"\"{\\\"user\\\":{\\\"name\\\":\\\"Ada\\\"},\\\"ok\\\":true}\""
Contar as barras antes de uma aspa diz quantas vezes remover o escape. Três significa duas vezes.

Recuperar os dados

Remover o escape é o inverso, uma camada por vez. Coloque a string escapada e você recupera o documento, já formatado:

  • Um toque remove uma camada. Uma string duplamente codificada volta como uma string ainda escapada, não como o documento. Toque de novo.
  • O botão fica desabilitado quando não há nada a remover. Ele só acende quando a entrada é parseada como uma string que por sua vez é parseada como JSON, então um clique acidental não estraga um documento normal.
  • Se você cola dados escapados, o formatador percebe. Ele avisa em vez de reformatar em silêncio, porque uma string que por acaso contém JSON e um documento JSON são coisas diferentes.

Entrada escapada

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

Depois de remover os escapes

{
  "user": {
    "name": "Ada"
  },
  "ok": true
}
Saída real do botão Remover escapes do formatador.

Evitar desde o começo

Codificação aninhada é quase sempre acidental. Se você controla o código que a produz, estas são as correções, na ordem de quanto ajudam.

  • Registre estruturado, não serializado. A maioria dos loggers aceita um objeto para um campo. logger.info({ body: payload }) aninha corretamente; JSON.stringify(payload) cria a camada que você depois tem que descascar.
  • Não serialize antes de guardar em uma coluna JSON. O jsonb do Postgres e equivalentes recebem o valor direto. Serializar primeiro guarda uma string, e toda consulta contra ela precisa de um cast.
  • Nunca construa JSON concatenando strings. É de lá que vem o escape insuficiente: uma aspa no sobrenome de alguém termina a string antes da hora e todo o payload fica inválido. Serialize com uma biblioteca.
  • Confira os payloads de filas de mensagens. Um corpo que já é JSON não precisa ser recodificado ao entrar numa fila que transporta texto.