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.
| Escape | Significa |
|---|---|
\" | Uma aspa dupla, não o fim da string |
\\ | Uma barra invertida literal |
\n | Nova linha |
\r | Retorno de carro |
\t | Tabulação |
\b \f | Backspace e form feed |
\/ | Uma barra normal. Legal, mas nunca obrigatória |
\uXXXX | Qualquer 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.comestá 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
\te\n; algo mais obscuro vira do tipo\u0001.
JSON de entrada
{"s":"café 中文 😀"}Depois do escape
"{\"s\":\"café 中文 😀\"}"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\"}"\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}"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}\""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
}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
jsonbdo 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.
Experimente, ou continue lendo
- Formatador JSONTem Escapar e Remover escapes na barra, e detecta entrada escapada ao colar.
- Validação e Schema JSONA outra metade do motivo pelo qual um payload é rejeitado.
- JSON pode ter comentários?Outra coisa que a especificação não permite, e o que fazer a respeito.
- Guia completo de JSONSintaxe, tipos e as regras por trás dos erros.