Mesmo JSON, quatro ferramentas, quatro resultados: como escolher um formatador
Resumo: Formatar não é sem perdas. O mesmo JSON com zero à esquerda, BOM ou inteiro de 20 dígitos produz resultados diferentes em Python, Node, jq e json.tool: alguns geram erro, outros alteram o valor silenciosamente. Dez amostras executadas localmente em quatro caminhos geram uma tabela de escolha e um script de validação rigorosa.

Conclusão: a diferença mais grave entre formatadores JSON não é quantos espaços eles indentam, mas o fato de que a mesma entrada pode gerar erro aqui e reescrever os dados em silêncio ali. Uma única amostra com valores-limite ensina mais que qualquer lista de recursos.
Costuma-se pensar que formatar é só «reorganizar o texto». Na prática, cada caminho o reinterpreta com seu próprio analisador. Quando o analisador é tolerante, o erro que deveria aparecer vira uma alteração silenciosa de valor, enquanto a página continua exibindo um verde tranquilizador de «Formatado com sucesso».
1. O fato: uma entrada, quatro caminhos, quatro resultados
Teste com Python 3.11.1, Node v22.13.1 e jq 1.7. Dez strings JSON curtas, construídas manualmente, foram enviadas pela entrada padrão, sem gravar em disco nem alterar configuração. Caminhos: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR indica exceção e parada; SILENT indica sem erro, mas saída diferente da entrada.
| Amostra | python json.loads | node JSON.parse | jq -c . | python -m json.tool |
|---|---|---|---|---|
S1 {"a":1,"b":[1,2]} |
OK | OK | OK | OK |
S2 {"a":1,} |
ERR | ERR | ERR | ERR |
S3 {'a':1} |
ERR | ERR | ERR | ERR |
S4 {"a":01} |
ERR char 6 | ERR position 6 | SILENT {"a":1} |
ERR |
S5 {"a":1 /*x*/} |
ERR char 7 | ERR | ERR | ERR |
S6 \ufeff{"a":1} |
ERR utf-8-sig | ERR position 0 | SILENT {"a":1} |
ERR |
S7 {"id":9007199254740993} |
OK fiel | SILENT 9007199254740992 | OK fiel | OK |
S8 {"a":1,"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
OK |
S9 {"a":1 |
ERR char 6 | ERR | ERR EOF | ERR |
S10 <html>502</html> |
ERR char 0 | ERR Unexpected token '<' | ERR | ERR |
Quatro das dez amostras deram «sem erro, mas dados alterados»: S4, S6, S7 e S8. Quem confia apenas na luz verde tem quarenta por cento de chance de segurar algo diferente do original. O pior: nenhum desses casos aparece numa lista de erros de sintaxe. O arquivo abre, copia e exporta normalmente; a falha está no conteúdo, não na sintaxe.
2. A causa: sintaxe e semântica são camadas diferentes
A RFC 8259 é explícita: a seção 1.2 recomenda evitar, por interoperabilidade, práticas que a ECMA-404 permite; a seção 4 exige nomes únicos apenas como SHOULD, sem definir o tratamento de chaves duplicadas; a seção 6 dá apenas recomendações sobre números. A ECMA-404 vai além e se declara «agnóstica quanto à semântica dos números»: especifica a representação, não o armazenamento. A norma deixa, portanto, uma fronteira ampla para quem implementa.
Assim, «formatar» se divide em duas camadas: a sintática decide se o texto é válido, e a semântica decide qual valor ele se torna. A primeira tem resposta padrão; a segunda, não. O jq ler 01 como 1 e saltar um BOM inicial é escolha semântica; o Node empurrar um inteiro de 20 dígitos para um double IEEE 754 é escolha do runtime; as quatro rotas manterem o último valor de {"a":1,"a":2} é convenção comum dos analisadores. Nada disso viola «analisável», mas tudo altera os dados.
Como corrigir erros de análise JSON atua na camada sintática e traduz a mensagem de erro em coordenadas. Este artigo trata do passo anterior: se a ferramenta não gera erro algum, você não obtém sequer coordenadas.
3. Quatro reescritas silenciosas, uma a uma
Isoladas, as quatro amostras mostram mudanças pequenas demais para perceber numa leitura rápida:
{"a": 01} // zero à esquerda: jq sai {"a":1}
\ufeff{"a": 1} // BOM: jq salta os três primeiros bytes
{"id": 9007199254740993} // inteiro de 20 dígitos: JSON.parse sai ...992
{"a": 1, "a": 2} // chave duplicada: as quatro mantêm {"a":2}
Zero à esquerda e BOM são do tipo «a entrada já era inválida, mas a ferramenta foi tolerante». 01 é ilegal em JSON padrão (RFC 8259 §6) e um BOM não deve abrir um texto JSON; Python e Node rejeitam, jq continua. Útil para limpar logs, desastroso na validação de contrato: o upstream acha que enviou dados inválidos a corrigir, enquanto o downstream já tem um resultado «aparentemente normal».
O inteiro grande é «entrada perfeitamente válida, mas o runtime não a comporta». 9007199254740993 é um número JSON válido, mas acima de 2^53-1, e JSON.parse só representa 9007199254740992. O int do Python e o jq preservam o valor. Se for número de pedido ou Snowflake ID, um último dígito alterado des casa a conciliação.
Chaves duplicadas são «a norma não diz, cada implementação escolhe». As quatro rotas ficam em silêncio com o último valor, sem aviso. A RFC 8259 diz apenas SHOULD, então não é bug — mas é a classe de problema mais difícil de rastrear ao concatenar configs ou mesclar patches.
4. Relatório de erro: quem dá posição, quem dá uma linha
Nas seis amostras com falha, a informação também varia muito. A JSONDecodeError do Python expõe msg, lineno, colno e pos; o Node v22 informa position N (line 1 column M) com trecho do original; python -m json.tool reaproveita os dados do Python; o jq dá só parse error: ... at line N, sem deslocamento de caractere.
Em JSON minificado de uma linha, linha e coluna quase não servem: tudo está na linha 1. O que se precisa é um deslocamento de caractere. O script abaixo normaliza a validação em «erro + offset + contexto»:
import json, sys
def strict_check(text: str) -> None:
try:
json.loads(text)
print("OK")
except json.JSONDecodeError as e:
print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
print("...", text[lo:hi].replace("\n", "\\n"), "...")
sys.exit(1)
strict_check(open(sys.argv[1], encoding="utf-8").read())
É mais rígido que jq . (rejeita zero à esquerda e BOM) e entrega e.pos. Como converter line 1 column 23611 em offset de byte e por que offsets de caractere e de byte diferem por um fator três em documentos CJK está em Depurando JSON minificado; não se repete aqui.
5. A correção: separar «validação estrita» e «embelezar»
A conclusão é simples: um botão «Formatar» não deve acumular validação e embelezamento. Primeiro um analisador estrito decide a validade; depois se escolhe a apresentação. Uma linha isola o primeiro passo:
python3 -m json.tool input.json > /dev/null && echo "sintaxe OK" || echo "sintaxe invalida"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json
A primeira responde só «é analisável?»; a segunda confere «a estrutura é a esperada?». Com inteiros grandes, leia com Python ou jq em vez de JSON.parse; com possível BOM, abra explicitamente com encoding='utf-8-sig' em vez de esperar um salto automático; com chaves possivelmente repetidas, varra antes de analisar, pois nenhuma das quatro rotas avisa. Levado ao CI, é exatamente o pipeline de Editores, linha de comando e cadeias de automação com uma camada extra de asserções de fidelidade.
6. Como escolher: tabela das quatro rotas
| Caminho | Força | Risco | Ideal para |
|---|---|---|---|
python json.loads / json.tool |
Estrito, dá linha/coluna e offset, inteiros sem perda | Rejeita zero à esquerda e BOM, codificação explícita | Validação de contrato, CI, triagem de logs |
node JSON.parse |
Nativo no navegador, erro com trecho da fonte | Inteiros grandes truncados em silêncio, erro com BOM | Depuração front, ferramentas no navegador |
jq -c . |
Amigável a pipes, resultado mais rápido | Zero à esquerda e BOM passam em silêncio, sem offset | Pipelines de operação, extração rápida |
Ferramentas web /parse, /editor |
Árvore, ordenação de chaves, copiar caminho, sem instalação | Depende do analisador da página, verificar uma vez | Inspeção manual, diagnóstico pontual |
A questão não é «quem tem mais recursos», mas «vai mentir sobre meus dados?». Com IDs de pedido, valores ou IDs de dispositivo, prefira Python ou uma ferramenta que declare fidelidade; para depurar estrutura sem persistir, o navegador basta. O /parse deste site valida e formata no navegador, ideal para «ver o formato», mas com inteiros grandes reconfirme com o script acima.
7. Privacidade é só uma das fronteiras
«Meus dados serão enviados?» é a pergunta mais comum sobre ferramentas online, mas é independente de «meus dados serão alterados?». Uma ferramenta local offline pode truncar um inteiro de 20 dígitos; inversamente, uma ferramenta online que erroa com rigor pode dar POST do texto original. Verifique as duas propriedades separadamente; uma não substitui a outra. A FAQ de privacidade dá três checagens de dois minutos; este artigo traz a outra metade: com a lista de amostras abaixo, confirme que a ferramenta não reescreve valores em silêncio.
8. Lista de autoverificação copiável
Salve S1–S10 em cases.txt, uma linha por amostra, cole em cada candidato e confira três pontos: as amostras inválidas falham de fato, a falha indica posição concreta, e a saída de uma amostra válida coincide caractere a caractere com a entrada. Atenção especial a S4, S6, S7 e S8: uma única linha SILENT significa que esse caminho não serve sozinho para validar dados.
Acrescente três restrições de engenharia: coloque a validação estrita num hook pre-commit em vez de descobrir a divergência em produção; grave arquivos como UTF-8 sem BOM em todos os idiomas; diante de JSON de terceiros, preserve o corpo bruto da resposta e use o resultado formatado só para leitura, nunca para reescrita.
Perguntas frequentes
Um formato bem-sucedido significa que o JSON está bom?
Não. S4, S6, S7 e S8 «passaram» no formato, mas zero à esquerda, BOM, inteiro de 20 dígitos e chave duplicada foram reescritos ou engolidos. Formatar só prova que algum analisador aceitou o texto, não que os valores igualam os enviados. Faça asserções valor a valor nos campos críticos.
Por que o jq aceita zero à esquerda e BOM que o Python rejeita?
O jq tem analisador léxico próprio: é tolerante com números como 01 e salta um BOM inicial, enquanto Python e Node seguem rigorosamente a RFC 8259. Não é bug de versão, é o compromisso entre analisador tolerante e estrito. O primeiro poupa trabalho na limpeza; o segundo é obrigatório na validação de contrato.
Como impedir que um inteiro grande vire 9007199254740992?
O Number do JavaScript é um double IEEE 754 com limite seguro 2^53-1. Ler um ID de 20 dígitos com JSON.parse trunca inevitavelmente. Armazene esses IDs como string, ou use o int do Python, jq ou um analisador com BigInt. Se o front realmente precisa de número, substitua o campo por string antes de analisar.
Ferramenta online ou linha de comando local?
Por sensibilidade e propósito: para ver estrutura ou achar um campo, o navegador é o mais rápido; para pipelines e asserções de fidelidade, python -m json.tool mais um verificador estrito. Em ambos os casos rode antes S1–S10, confirme erros e saídas e só então decida qual classe de dados inserir.