FAQ de sintaxe e formatos: comentários, vírgulas finais e precisão
Resumo: Se comentários são permitidos, por que uma vírgula final dá erro, se chaves precisam de aspas, por que 9007199254740993 muda — doze conclusões, cada uma conferida contra a RFC 8259.

Resumo da ópera: escreva JSON puro da RFC 8259 — aspas duplas, sem comentários, sem vírgulas finais, UTF-8 sem BOM — para qualquer coisa que cruze a fronteira de um processo; use JSONC ou JSON5 para configuração que precisa de comentários e converta de volta em tempo de execução; e serialize inteiros acima de 2^53-1 como strings no servidor.
O que essas perguntas têm em comum: elas parecem que "deveriam funcionar", mas a especificação é na verdade muito estrita.
1. O que conta como JSON válido
Qual é a diferença entre JSON, JSON5 e JSONC?
O JSON padrão reconhece apenas strings com aspas duplas e não permite comentários nem vírgulas finais. JSON5 e JSONC adicionam recursos em cima dele:
| Recurso | JSON (RFC 8259) | JSONC | JSON5 |
|---|---|---|---|
| Strings com aspas duplas | Suportado | Suportado | Suportado |
| Strings com aspas simples | Não suportado | Não suportado | Suportado |
| Comentários | Não suportado | Suportado | Suportado |
| Vírgulas finais | Não suportado | Não suportado | Suportado |
| Chaves sem aspas | Não suportado | Não suportado | Suportado |
| Números hexadecimais | Não suportado | Não suportado | Suportado |
NaN / Infinity |
Não suportado | Não suportado | Suportado |
| Extensão de arquivo | .json |
.jsonc |
.json5 |
O princípio de seleção é simples: use sempre JSON padrão ao cruzar fronteiras de processo, enquanto arquivos de configuração podem usar JSONC ou JSON5 conforme o que seu editor suporta.
tsconfig.json e .vscode/settings.json podem conter comentários porque o editor os analisa como JSONC; alimentá-los a um
JSON.parse estrito ainda dá erro.
JSON pode conter comentários?
Não. A gramática da RFC 8259 não tem produção de comentário, então qualquer // ou /* */ faz a análise falhar.
Se você quer manter explicações na configuração, há três alternativas:
{
"_comment": "以下超时单位是毫秒",
"timeout": 3000,
"retry": {
"$doc": "重试次数,0 表示不重试",
"count": 3
}
}
- Use campos convencionais como
_commentque o leitor simplesmente ignora; - Use campos com prefixo
$para reduzir a chance de colidir com campos de negócio; - Simplesmente mude para JSONC ou YAML para configuração e converta para JSON padrão em tempo de execução.
Por que uma vírgula final causa erro?
Porque na gramática tanto arrays quanto objetos são estruturas de "elemento + separador", e nenhuma vírgula é permitida depois do último elemento.
{
"a": 1,
"b": 2,
}
O trecho acima tem uma vírgula extra antes de }, e o erro normalmente é relatado na vírgula ou na chave de fechamento, dependendo da implementação do analisador.
Esta é também a armadilha mais fácil ao migrar do JavaScript — literais de objeto JS permitem vírgulas finais, o JSON não.
Aspas simples são permitidas?
Não. Strings precisam usar aspas duplas; 'text' é um erro de sintaxe.
Se os dados foram copiados do repr() do Python, de um script de shell ou de uma configuração escrita à mão, converta primeiro com uma ferramenta
em vez de esperar que o analisador seja leniente.
As chaves podem ficar sem aspas?
Não. Nomes de membros de objeto precisam ser strings entre aspas duplas.
{ name: "json" }
Isso é um literal de objeto JavaScript, não JSON. Adicionar aspas o torna válido:
{ "name": "json" }
O nível superior pode ser um array ou uma string?
Sim. A RFC 8259 permite qualquer valor JSON no nível superior: objetos, arrays, strings, números, true, false e null são todos válidos.
Na prática da engenharia, porém, recomenda-se usar apenas objetos ou arrays no nível superior, por dois motivos:
- Compatibilidade: analisadores mais antigos (da era da RFC 4627) exigem que o nível superior seja um objeto ou um array;
- Segurança: um array de nível superior historicamente carregava risco de JSON hijacking. Navegadores modernos corrigiram, mas sistemas antigos ainda presumem a convenção.
Então "hello" é JSON válido e ["a", "b"] também, mas uma API retornando {"data": [...]} é mais segura.
2. Codificação e strings
Um BOM no início do arquivo quebra a análise?
Sim. A RFC 8259 exige explicitamente que o texto JSON seja transmitido como UTF-8 e não deve acrescentar uma marca de ordem de bytes (BOM).
O BOM são os três bytes EF BB BF no início absoluto do arquivo. Ele é completamente invisível, mas ocupa a primeira posição da string,
então o analisador conclui que "o primeiro caractere não é {" e dá erro — e a linha e coluna relatadas normalmente são linha 1, coluna 1, o que parece desconcertante.
O "Salvar como UTF-8" do Bloco de Notas do Windows inclui um BOM por padrão. Para verificar e remover:
# Verificar: saída começando com efbbbf significa que há um BOM
head -c 3 data.json | xxd
# Remover: pule os primeiros 3 bytes com tail
tail -c +4 data.json > clean.json
O JSON.parse do JavaScript moderno tolera um BOM (o V8 o pula internamente), mas implementações estritas em Java, Go e Python não,
o que é uma razão comum para "o mesmo arquivo dá resultados diferentes em linguagens diferentes".
Caracteres chineses precisam ser convertidos para \uXXXX?
Não. A RFC 8259 exige que o texto JSON seja UTF-8, então o chinês pode aparecer diretamente; {"name": "张三"} é perfeitamente válido.
\uXXXX é apenas uma grafia alternativa equivalente, usada para canais que não conseguem representar o caractere diretamente. A regra geral:
- Corpos de requisição HTTP, arquivos, bancos de dados: escreva o chinês diretamente, economizando 3 bytes por caractere;
- Cabeçalhos HTTP, parâmetros de URL, embutir dentro de outra string: use escapes para evitar problemas de codificação.
Os únicos caracteres que precisam ser escapados são os caracteres de controle (U+0000 a U+001F). Por exemplo, uma quebra de linha precisa ser escrita como \n;
você não pode colocar uma quebra de linha literal dentro de uma string.
Emojis podem ir direto para uma string?
Sim, mas lembre que eles ocupam duas unidades de código UTF-16.
Emojis e muitos caracteres raros (como 𠮷) ficam fora do BMP, então em JavaScript "😀".length === 2.
Validar por contagem de caracteres é, portanto, fácil de errar:
const text = "😀";
console.log(text.length); // 2, número de unidades de código UTF-16
console.log([...text].length); // 1, número de pontos de código
console.log(Buffer.byteLength(text)); // 4, número de bytes UTF-8
Se um limite de comprimento de apelido é calculado com .length, um emoji consome dois espaços; se o backend conta bytes, são quatro.
Ao alinhar definições, declare claramente se você quer dizer pontos de código, unidades de código ou bytes.
3. Números e valores especiais
Por que o inteiro grande 9007199254740993 virou 9007199254740992?
Porque o JSON.parse do JavaScript converte números em floats de precisão dupla IEEE 754, e o maior inteiro seguro é 2^53-1.
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
9007199254740993 // na verdade produz 9007199254740992
Qualquer número acima de 2^53-1 pode ser arredondado nos últimos dígitos. Vítimas típicas são IDs de usuário de 19 dígitos gerados por algoritmos snowflake, números de pedido e timestamps em nanossegundos.
Três correções, ordenadas por preferência:
- Serialize o ID como string no servidor:
"id": "9007199254740993", removendo o problema de precisão na raiz; - Use uma biblioteca de análise sem perdas: substitutos do
JSON.parseconseguem preservar inteiros grandes literalmente, por exemplojson-bigint; - Extraia com uma expressão regular apenas para exibição: não analise o documento inteiro, apenas case o campo alvo como texto.
Observe que JSON.stringify tem o problema simétrico: um número fora da faixa de inteiro seguro pode não manter o valor original após a serialização.
NaN e Infinity podem entrar em JSON?
Não. A produção numérica do JSON permite apenas notação decimal, e NaN, Infinity e -Infinity não são valores JSON válidos.
A abordagem do JavaScript é "degradação silenciosa":
JSON.stringify({ value: NaN }); // '{"value":null}'
JSON.stringify({ value: Infinity }); // '{"value":null}'
Sem erro, NaN simplesmente vira null. Esta é a cena clássica em que "o frontend enviou NaN e o backend recebeu null".
Quando você precisa transmitir essa semântica, expresse-a explicitamente com strings ou valores sentinela acordados:
{ "ratio": null, "ratioState": "not-a-number" }
Números podem ser escritos como 1e3 ou 0.5?
Sim. Números JSON suportam decimais e expoentes, então 1e3, 1E3, 0.5 e -0.5 são todos válidos.
Mas há restrições:
- Não podem começar nem terminar com
.:.5e5.são ambos inválidos, então escreva0.5e5; - Sem zeros à esquerda:
01é inválido; - Sem hexadecimal:
0x1Fé inválido, isso é uma extensão do JSON5; -0é válido, masJSON.stringify(-0)produz0, perdendo o sinal.
4. Semântica de objetos
O que acontece com chaves duplicadas?
O comportamento é definido pela implementação; a RFC 8259 só diz que nomes de chave "devem" ser únicos e não especifica o que acontece com duplicatas.
Na prática, a grande maioria das implementações pega o último valor:
JSON.parse('{"a":1,"a":2}') // { a: 2 }
Mas você não pode contar com isso. Algumas bibliotecas Java, o encoding/json do Go e o json do Python diferem entre versões:
algumas pegam o primeiro valor, algumas dão erro e algumas pegam o último. Tratar chaves duplicadas como erro é a abordagem mais segura entre linguagens.
Você não consegue achar chaves duplicadas a olho nu — em JSON minificado, a mesma chave pode estar a milhares de caracteres de distância. Use um script:
# Extrair chaves que aparecem mais de uma vez no mesmo nível, como verificação aproximada
jq -r 'paths(scalars) | join(".")' data.json | sort | uniq -d
Em que undefined se transforma?
Depende, e este é o ponto mais propenso a bugs:
JSON.stringify({ a: undefined }); // '{}' a chave é descartada por completo
JSON.stringify({ a: undefined, b: 1 }); // '{"b":1}'
JSON.stringify([undefined]); // '[null]' vira null dentro de arrays
JSON.stringify({ a: function () {} }); // '{}' funções também são descartadas
O mesmo undefined significa "o campo desaparece" dentro de um objeto e "vira null" dentro de um array.
Se o backend toma ramos diferentes para "campo ausente" e "campo é null", você tem uma inconsistência aqui.
Quando precisar expressar "sem valor" explicitamente, use null em vez de undefined.
O que acontece após analisar {"__proto__": {...}}?
O JSON.parse em si é seguro: __proto__ é tratado como uma propriedade própria comum e não altera o protótipo.
O que é perigoso é a operação de merge depois da análise:
const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch); // algumas implementações poluem o protótipo
A abordagem correta é iterar as propriedades próprias e pular __proto__, constructor e prototype.
Detalhes na FAQ de desempenho e segurança.
5. Referência rápida
| Sintaxe | Válido | Observações |
|---|---|---|
{"a": 1,} |
Não | Vírgula final |
{'a': 1} |
Não | Aspas simples |
{a: 1} |
Não | Chave sem aspas |
{"a": 1} // comment |
Não | Comentários |
"hello" |
Sim | Uma string de nível superior é válida, mas raramente usada em APIs |
{"a": 1e3} |
Sim | Expoentes são suportados |
{"a": 01} |
Não | Zero à esquerda |
{"a": NaN} |
Não | Literal inválido; stringify o transforma em null |
\uFEFF{"a":1} |
Não | Um BOM não faz parte do texto JSON |
{"a":1,"a":2} |
Contestado | As implementações pegam o último valor, mas não conte com isso |