JSON Toolkit

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.

Intermediate~6 min de leituraPublicado em 2026-11-03Atualizado em 2026-11-03
Quatro caminhos de análise retornando resultados diferentes para o mesmo JSON

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.