Escapar uma string para JSON: três carateres são obrigatórios, e um é uma armadilha
Publicado a 31/07/2026 · 13 min de leitura · Ferramentas para programadores
Daniel Okonkwo — Programador front-end e redator de Tecnologia na Allin
Desempenho web · Formatos de ficheiro
Verificado a partir de 4 fontes
A RFC 8259, secção 7, nomeia três coisas que TÊM de ser escapadas dentro de uma string JSON e mais nenhuma: as aspas, a contrabarra e todo o carácter de controlo de U+0000 a U+001F. Tudo o resto pode ficar como UTF-8 literal — letras acentuadas, ideogramas, emoji, o carácter DEL em U+007F, o separador de linha em U+2028. Qualquer carácter PODE ser escapado, e daí vem o hábito de escrever \/ para uma barra: o JSON nunca o exige, e a única razão para o fazer é embeber JSON dentro de uma etiqueta script de HTML, onde a sequência </ tem de ser partida. Existem oito escapes de dois carateres — \" \\ \/ \b \f \n \r \t — e tudo o resto usa \uXXXX, quatro dígitos hexadecimais, uma unidade UTF-16. Um carácter fora do Plano Multilingue Básico precisa de dois: um emoji escreve-se como par substituto de doze carateres, nunca como escape de seis dígitos. É aí que está a armadilha. As strings JSON são sequências de unidades UTF-16, e a gramática admite uma unidade que é metade de um par sem companheiro — a especificação di-lo na secção 8.2, com � por exemplo. Um texto JSON assim analisa-se sem reclamar. Mas a secção 8.1 exige que o JSON trocado entre sistemas seja UTF-8, e a RFC 3629, secção 3, proíbe o UTF-8 de codificar seja o que for entre U+D800 e U+DFFF. Medido nesta ferramenta: o texto {"k":"id-�-end"} analisa-se, e codificar o resultado em UTF-8 dá os bytes 69 64 2d ef bf bd 2d 65 6e 64 — a metade solta passou a U+FFFD, o carácter de substituição, e o valor já não faz a ida e volta. Nada lançou um erro. É essa a falha a procurar.
A RFC 8259 exige escapar exatamente três coisas dentro de uma string JSON. Tudo o resto é facultativo. O que parte mesmo os pipelines é um meio-substituto solto: legal no texto JSON, impossível em UTF-8, e substituído em silêncio assim que os seus dados são escritos.
Três obrigatórios, tudo o resto facultativo
A regra é mais curta do que quase toda a gente supõe. Todos os carateres Unicode podem ficar entre as aspas exceto os que têm de ser escapados: as próprias aspas, a contrabarra e os carateres de controlo de U+0000 a U+001F. Três elementos. Um caminho de Windows cheio de contrabarras e aspas, C:\Users\Léa\"relatório".txt, exige cada contrabarra duplicada e cada aspa escapada, e o é fica tal e qual. Não há regra sobre acentos, nem sobre escritas não latinas, nem sobre emoji.
Dois limites merecem ser verificados em vez de adivinhados. O carácter DEL em U+007F não é um carácter de controlo para este efeito — o intervalo pára em U+001F — por isso fica literal, e passá-lo pela ferramenta confirma-o: a saída é o próprio carácter, um byte em UTF-8, sem escape. O separador de linha U+2028 e o separador de parágrafo U+2029 ficam igualmente intactos, porque também não estão no intervalo U+0000 a U+001F. Foi outrora um perigo real quando se colava JSON dentro de um script em linha, já que um literal de string de JavaScript não os podia conter; a linguagem foi alterada em 2019 para o permitir. Se a sua saída ainda tem de sobreviver a um analisador antigo, o modo só-ASCII da ferramenta escapa-os como \u2028 e \u2029, tal como tudo o que passe do intervalo ASCII.
A barra com escape merece um parágrafo, porque é a coisa mais comum que as pessoas fazem sem saber porquê. O JSON permite \/ e nunca o exige. O hábito vem de meter JSON dentro de um elemento script de HTML, onde um </script> literal dentro de uma string fecharia o elemento antes de tempo e entregaria o resto dos seus dados ao analisador de HTML; escapar a barra parte a sequência e o navegador nunca a vê. A ferramenta tem um interruptor para isso, desligado por omissão, que é o valor certo: ligue-o quando estiver a embeber, deixe-o desligado em todo o resto. Escapar barras numa resposta de API engorda a carga útil e não muda um único valor.
Unidades UTF-16, não carateres
O escape \uXXXX leva exatamente quatro dígitos hexadecimais, que é uma unidade UTF-16 e cobre de U+0000 a U+FFFF. Tudo o que esteja acima tem de se escrever com dois deles, e a especificação é explícita: para escapar um carácter estendido que não esteja no Plano Multilingue de Base, ele é representado como uma sequência de doze carateres que codifica o par substituto UTF-16. Não existe forma de seis dígitos. Passe uma cara sorridente pela ferramenta em modo ASCII e ela produz 😀, o que está certo; se algum dia vir \u1f600 na saída de alguém, isso não é JSON, é outra convenção de escape que se infiltrou vinda do Python ou de uma shell.
É por isso que a ferramenta mostra quatro contagens diferentes para a mesma string e por isso é que divergem. Uma cara sorridente é um ponto de código, duas unidades UTF-16 e quatro bytes UTF-8. Um emoji de família feito de quatro figuras unidas por juntores de largura zero são sete pontos de código, onze unidades UTF-16 e vinte e cinco bytes UTF-8. Se a sua coluna de base de dados está declarada com vinte carateres, qual desses três números isso significa é uma propriedade da base de dados e não dos seus dados, e é na diferença entre eles que vivem os erros de truncatura.
O meio-substituto solto, e como chega aos seus dados
Um par substituto são duas unidades de código que só significam algo juntas: uma metade alta de U+D800 a U+DBFF seguida de uma metade baixa de U+DC00 a U+DFFF. A gramática do JSON não impõe o emparelhamento. A secção 8.2 da RFC 8259 di-lo por palavras, notando que a especificação permite que os valores de string contenham sequências de bits que não conseguem codificar carateres Unicode, e dando � por exemplo. Assim {"k":"id-�-end"} é um texto JSON sintaticamente válido. Qualquer analisador com que se cruze aceita-o.
A contradição chega uma secção antes. A secção 8.1 exige que o texto JSON trocado entre sistemas que não fazem parte de um ecossistema fechado seja codificado em UTF-8, e a RFC 3629, que define o UTF-8, proíbe codificar números de carácter entre U+D800 e U+DFFF, precisamente porque estão reservados para o UTF-16. Assim, um valor que o JSON permite não pode exprimir-se na codificação que o JSON impõe. O que as implementações fazem em vez de falhar é substituir: passe esse texto JSON por um analisador e recodifique o resultado, e os bytes voltam como 69 64 2d ef bf bd 2d 65 6e 64. Esses três bytes do meio, EF BF BD, são U+FFFD, o carácter de substituição. O valor que entrou não é o que saiu, e em lado nenhum se levantou um erro.
Ninguém escreve um meio-substituto solto. Chegam por truncatura. Pegue na string Rapport 📊 final, dezasseis unidades UTF-16 para quinze pontos de código, e corte-a a nove unidades para caber numa etiqueta: o resultado é Rapport seguido de meio emoji, que a ferramenta escapa como Rapport �. É essa a origem do dia a dia: uma coluna de base de dados com limite de carateres, uma interface que corta um título, uma linha de registo cortada a uma largura fixa, uma importação que copia campos de comprimento fixo. Onde quer que uma string seja cortada a contar unidades em vez de pontos de código, o corte pode cair a meio de um par.
A ferramenta dá-lhe duas maneiras de detetar um antes que ele viaje. O escapador escreve sempre um meio-substituto solto na forma \uXXXX, mesmo no seu modo por omissão não-ASCII — exatamente o que faz um JSON.stringify moderno — por isso um � ou um � inesperado na saída é o sinal. E o contador de bytes, que dá os mesmos números que um codificador UTF-8 real, já terá contado essa metade como três bytes: três bytes é o que custa o U+FFFD, pelo que uma string cuja forma escapada contém um meio-substituto já está a ser cobrada por um carácter de substituição antes sequer de sair do seu ecrã.
Onde esta ferramenta é deliberadamente mais permissiva do que um analisador
No sentido do escape, a saída por omissão da ferramenta coincide, carácter a carácter, com o que produz um JSON.stringify moderno, em todos os casos experimentados: carateres de controlo, emoji, acentos combinantes, caminhos Windows, meios-substitutos soltos. Se só usar as definições por omissão, o resultado é o corpo de uma string JSON sem as aspas exteriores, e nada mais surpreendente do que isso.
No sentido de desfazer o escape é deliberadamente mais frouxa, e vale a pena saber onde. Um analisador JSON a sério rejeita um carácter de controlo cru dentro de uma string: cole um carácter de campainha literal num documento JSON e obtém um erro de sintaxe a dizê-lo. Esta ferramenta aceita-o e devolve-o, com o argumento de que está a inspecionar um fragmento e não a validar um documento. Passar uma string pelo modo de desescape sem queixas não prova, portanto, que o JSON à volta seja válido. Rejeita, isso sim, as três coisas que tornam um fragmento absurdo: um escape desconhecido como \x, um \u incompleto ou não hexadecimal, e uma contrabarra pendurada mesmo no fim, e indica a posição e a sequência culpada em cada caso.
O interruptor só-ASCII é a outra definição que vale a pena perceber, porque muda o tamanho da sua carga útil e não o seu significado. Escapar tudo o que passa do intervalo ASCII torna a saída transportável através de um sistema de codificação incerta, com um custo real: Café passa de quatro carateres para nove, e um emoji sozinho de dois para doze. Use-o quando o transporte for duvidoso — um agregador de registos antigo, um URL, um cabeçalho, uma base de dados cuja colação não lhe merece confiança — e não de outra forma, porque o UTF-8 é o que a secção 8.1 pede e ocupa menos.
| Entrada | Exigido pela RFC 8259? | Saída da ferramenta (modo por omissão) |
|---|---|---|
| Umas aspas | Sim, obrigatório | Escape de dois carateres |
| Uma contrabarra | Sim, obrigatório | Duplicada |
| Uma tabulação, U+0009 | Sim, obrigatório (carácter de controlo) | A forma curta da tabulação, não um escape de seis carateres |
| O carácter de campainha, U+0007 | Sim, obrigatório (controlo sem forma curta) | Um escape de seis carateres a terminar em 0007 |
| DEL, U+007F | Não — o intervalo de controlo pára em U+001F | Deixado literal, um byte UTF-8 |
| Uma barra | Não — pode ser escapada, nunca obrigatório | Deixada tal e qual salvo se a opção da barra estiver ligada |
| Um emoji fora do BMP | Não — o UTF-8 literal serve | Mantido tal e qual; em modo ASCII, um par substituto de doze carateres |
| Um meio-substituto solto | Representável, mas não codificável em UTF-8 | Sempre escapado como \uXXXX, mesmo em modo por omissão — é o sinal de alarme |
Perguntas frequentes
- Que carateres tenho de escapar numa string JSON?
- Exatamente três espécies, segundo a RFC 8259 secção 7: as aspas, a contrabarra e todo o carácter de controlo de U+0000 a U+001F. Mais nada é obrigatório. Letras acentuadas, ideogramas, emoji, DEL em U+007F e os separadores U+2028 e U+2029 podem todos estar na string como UTF-8 literal. Dos carateres de controlo, cinco têm forma curta — retrocesso, avanço de página, mudança de linha, retorno de carro e tabulação — e os restantes precisam da forma \u de seis carateres, que é como se escreve uma campainha ou um byte nulo. Qualquer coisa pode ser escapada se quiser, e é por isso que às vezes chega JSON perfeitamente válido com cada carácter não-ASCII soletrado: é maior, não mais correto.
- Como escrevo um emoji numa string JSON?
- Duas maneiras, ambas corretas. Deixá-lo como UTF-8 literal: nada na especificação obriga a escapá-lo, e é o que a ferramenta faz por omissão. Ou, se a carga útil tiver de ser ASCII, escrever o par substituto: dois escapes \uXXXX, doze carateres ao todo, um para a metade alta e outro para a baixa. Não existe forma de seis dígitos: \u1f600 não é JSON, e um analisador que o aceite não está a fazer o que a norma diz. O custo da forma ASCII é real — um emoji passa de dois carateres para doze — por isso use-a só quando não puder confiar UTF-8 ao transporte.
- O que é um meio-substituto solto e porque é que parte tudo?
- É metade de uma codificação de duas unidades sem companheira: uma unidade de código entre U+D800 e U+DFFF por sua conta. O JSON permite-o — a RFC 8259 secção 8.2 reconhece explicitamente que os valores de string podem conter sequências de bits incapazes de codificar carateres Unicode, e dá um substituto sem par como exemplo — mas o UTF-8 não o consegue exprimir, porque a RFC 3629 proíbe codificar seja o que for nesse intervalo. Como a secção 8.1 exige UTF-8 para a troca, as duas regras chocam, e o que acontece na prática é uma substituição: a metade passa a U+FFFD, o carácter de substituição, três bytes, e o valor original desapareceu. Não é lançada exceção nenhuma pelo caminho, e é por isso que isto aparece dias depois como uma pesquisa que já não encontra nada ou um identificador que já não cruza.
- Devo escapar a barra?
- Só quando estiver a embeber o JSON dentro de um elemento script de HTML. O JSON permite \/ e nunca o exige; o escape existe porque um </script> literal dentro de um valor de string fecharia o elemento antes de tempo e entregaria o resto dos seus dados ao analisador de HTML como marcação. Partir a sequência com uma contrabarra impede isso. Em todos os outros sítios — uma resposta de API, um ficheiro em disco, uma mensagem numa fila — escapar barras só inflaciona a carga útil. A ferramenta vem com a opção desligada, que é o valor certo, e ligá-la não muda nada no significado do valor depois de analisado.
- A ferramenta aceitou a minha string mas o meu analisador rejeita-a. Porquê?
- Porque o sentido de desescape é deliberadamente mais tolerante do que um analisador de documentos, e a diferença são os carateres de controlo. Um analisador JSON rejeita um carácter de controlo cru posto literalmente dentro de uma string — a mensagem costuma dizer «bad control character in string literal» — ao passo que esta ferramenta o aceita e lho devolve, partindo do princípio de que está a inspecionar um fragmento e não a validar um documento. Se o seu analisador protesta e a ferramenta não, procure uma tabulação ou uma mudança de linha crua que devia ter sido escrita como escape. A ferramenta rejeita, isso sim, os três defeitos que tornam um fragmento ilegível: uma sequência de escape desconhecida, um \u incompleto ou não hexadecimal, e uma contrabarra pendurada no fim, e indica-lhe a posição de cada um.
Artigos que podem interessar-lhe
Todos os guias →Ferramentas relacionadas
Isto descreve como se comportam um formato de ficheiro e um motor de renderização, verificado face à especificação citada e ao código da ferramenta tal como está hoje. Os motores divergem: GitHub, GitLab, um gerador de sites estáticos e a pré-visualização do seu editor são quatro implementações diferentes, e o que funciona numa pode não funcionar noutra. Nada aqui é uma garantia sobre a sua cadeia de publicação — teste o resultado onde ele vai mesmo ser publicado, e trate qualquer ferramenta, esta incluída, como algo a verificar e não a acreditar.
Fontes
- RFC Editor — RFC 8259, The JavaScript Object Notation (JSON) Data Interchange Format, December 2017, Internet Standard STD 90 — section 7 lists the characters that MUST be escaped (quotation mark, reverse solidus, U+0000 to U+001F), states that any character MAY be escaped, and defines the \uXXXX form and the twelve-character surrogate pair; section 8.1 requires UTF-8 for interchange; section 8.2 acknowledges that the grammar permits an unpaired surrogate such as �
- RFC Editor — RFC 3629, UTF-8, a transformation format of ISO 10646, November 2003 — section 3 prohibits UTF-8 from encoding character numbers between U+D800 and U+DFFF because they are reserved for use with the UTF-16 encoding form
- Ecma International — ECMA-404, The JSON Data Interchange Syntax, 2nd edition, December 2017 — the syntax half of the same standard, published in parallel with RFC 8259 and freely downloadable
- Ecma International — ECMAScript Language Specification, JSON.stringify — since the well-formed JSON.stringify change of ES2019, a lone surrogate in a JavaScript string is emitted as a \uXXXX escape rather than as an unpaired code unit, which is the behaviour this tool reproduces
Detetaste um erro neste artigo?