JSON Toolkit

Quando dois documentos JSON diferem: diff posicional, diff por id ou normalizar primeiro

Resumo: Cinquenta registros viram cinquenta e um e o diff acusa cinquenta e uma mudanças. Cobre falsos positivos de diff de texto, casamento de arrays por id, normalização com jq e patches aplicáveis.

Iniciante~6 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa de comparar dois documentos JSON

Resumo da ópera: normalize primeiro e compare depois — use jq -S ou serialização estável para apagar ordem de chaves, indentação e grafia de números, ordene ou indexe arrays por uma chave de negócio quando a ordem não tiver significado, e só então transforme o diff em patch.

Uma diferença JSON não é uma diferença de texto. Compare os arquivos como strings e uma reordenação de chaves acende a página inteira; compare arrays por índice e inserir um registro marca os outros cinquenta como alterados.

Um patch foi enviado ao serviço de pedidos, e tudo o que você tem são duas respostas: cinquenta registros antes, cinquenta e um depois, com um novo registro inserido no meio. A ferramenta de diff acusa cinquenta e uma mudanças, uma parede de vermelho, e você ainda não consegue dizer qual registro realmente se moveu.

A ferramenta não está quebrada. "Comparar" significa três coisas diferentes em JSON, e a maioria das pessoas recorre por padrão à menos útil.

Reproduzindo o falso alarme: uma inserção, cinquenta e uma mudanças

Trinta linhas de Node 22: construa cinquenta pedidos ({id, sku, qty}), acrescente um no fim e insira um no início, contando diferenças com alinhamento índice a índice.

Operação Registros Mudanças relatadas por posição
Acrescentar um no fim 50 → 51 1
Inserir um no início 50 → 51 51

O mesmo "um registro extra", cinquenta e uma vezes o ruído. O motivo é banal: quando você compara por índice, o novo elemento cai na posição 0 e todo registro existente desliza um slot para baixo. Posição 0 versus posição 0, posição 1 versus posição 1 — nenhum par é igual.

Construa um Map chaveado por id e os mesmos dados relatam exatamente uma entrada: $[id=ORD-999] added.

Esse experimento lhe dá a regra que importa mais do que qualquer recomendação de ferramenta: se o array é uma sequência ordenada ou um conjunto não ordenado decide qual comparação você deve usar.

Três maneiras pelas quais um diff de texto mente sobre JSON

diff a.json b.json compara caracteres. JSON carrega uma estrutura de dados. Essa incompatibilidade produz três tipos confiáveis de falso positivo.

1. Chaves de objeto reordenadas

A seção 4 da RFC 8259 é explícita: um objeto é "uma coleção não ordenada de pares nome/valor". Estes dois documentos são o mesmo valor:

{ "id": 42, "name": "Ada", "active": true }
{ "active": true, "name": "Ada", "id": 42 }

Um diff de linhas chama as três linhas de alteradas. Eu inverti a ordem das chaves de um objeto de dez chaves, formatei ambos e comparei linha a linha: todas as dez linhas sinalizadas, enquanto uma comparação estrutural diz que são iguais. Proporção de ruído 10:0.

Por que a ordem das chaves muda afinal? Trocar de serializador, rodar um formatador com chaves ordenadas, reconstruir o objeto a partir de um dicionário ou hash map, mudar entre runtimes de linguagem — qualquer um desses basta para fazer dados idênticos parecerem diferentes em disco.

2. Espaços em branco e indentação

{"a":1} e {\n "a": 1\n} são analisados no mesmo valor. Um diff de texto vê quebras de linha e espaços e sinaliza tudo.

3. Grafias diferentes de números e escapes

1.0 e 1, 1e2 e 100, "\u00e9" e "é" — mesmo valor após a análise, texto diferente após a serialização. Este é mais ameno porque toca poucas linhas, mas em revisões de configuração basta para você pensar que um número mudou.

O caso inverso é o que o texto não consegue ver e a estrutura consegue: 5 versus "5". Um par extra de aspas, mas o tipo derivou — normalmente um bug em vez de uma atualização. Uma comparação estrutural relata isso como modify sem hesitar.

Três modos de comparação, três trabalhos

Modo O que é comparado De onde vem o ruído Saída Serve para
Diff de texto Linhas de caracteres Ordem de chaves, indentação, grafia de números Números de linha, vermelho/verde Pacotes de localização, fixtures com ordem de chaves estável
Diff estrutural Árvores analisadas, alinhadas por caminho Arrays alinhados por índice Caminhos com add/remove/modify Respostas de API, configs, webhooks
Diff semântico Árvores mais chaves de negócio Apenas uma escolha ruim de chave Caminhos com detecção de move/insert Listas de registros, pedidos, inventário

O diff estrutural resolve a camada "ordem de chaves e formatação". O diff semântico vai um passo além e resolve "identidade de elemento de array". A diferença não é complexidade de implementação; é se você está disposto a dizer à ferramenta qual campo é o passaporte de um registro.

Arrays são a linha divisória

Faça uma pergunta: a ordem deste array é, ela mesma, dado?

  • Ordem é dado: etapas de migração, rankings de busca, pontos de gráfico, cadeias de aprovação. Uma mudança de posição é uma mudança de comportamento, então a comparação posicional está correta.
  • Ordem não tem significado: tags, conjuntos de IDs de permissão, itens de linha de pedido, listas de inventário. Uma inserção não deveria afetar como os outros itens são julgados.

Quando a ordem não tem significado, indexe os registros por uma chave em vez de martelá-los por posição:

// Case por id: uma inserção ou exclusão relata uma entrada, os outros itens permanecem
function diffById(before, after, path = '$') {
  const out = [];
  const beforeMap = new Map(before.map((item) => [item.id, item]));
  const afterMap = new Map(after.map((item) => [item.id, item]));

  for (const [id, value] of afterMap) {
    if (!beforeMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'added' });
  }
  for (const [id, value] of beforeMap) {
    if (!afterMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'removed' });
    else if (JSON.stringify(value) !== JSON.stringify(afterMap.get(id))) {
      out.push({ path: `${path}[id=${id}]`, type: 'changed' });
    }
  }
  return out;
}

Bibliotecas maduras transformam isso em uma opção. O jsondiffpatch usa um LCS para encontrar a maior subsequência comum, mas elementos de objeto dentro de arrays são casados por posição por padrão — a documentação oficial enfatiza que você precisa fornecer uma função objectHash para casá-los por conteúdo. Usuários de primeira viagem muitas vezes concluem que a biblioteca é burra; na realidade eles nunca passaram uma chave.

Escolher a chave também importa. id é a escolha segura. sku funciona apenas quando os SKUs são únicos. Fazer hash do objeto inteiro transforma "um campo mudou" em "um removido mais um adicionado", o que é pior do que comparar por posição.

O truque de terminal mais rápido: normalizar com jq

Se você não quer uma biblioteca e só precisa de uma olhada, normalize primeiro e faça diff depois. jq -S ordena chaves de objeto recursivamente e aplica indentação consistente:

# Normalize e depois compare linha a linha; o ruído de ordem de chaves e indentação desaparece
diff -u <(jq -S . before.json) <(jq -S . after.json)

# Quando o array é genuinamente não ordenado, ordene por chave antes de comparar
diff -u <(jq -S '.items |= sort_by(.id)' before.json) \
        <(jq -S '.items |= sort_by(.id)' after.json)

Rodei printf '{"b":2,"a":1,"arr":[1,2]}' | jq -S . e obtive {"a":1,"arr":[1,2],"b":2} — chaves ordenadas, arr intocado. Lembre disto: -S ordena apenas chaves de objeto, nunca elementos de array. Ou ordene arrays explicitamente com sort / sort_by, ou aceite ruído posicional.

Duas armadilhas:

  • jq não consegue editar no lugar; ele grava na saída padrão. Use substituição de processo (<(...)), ou grave um arquivo temporário, verifique e depois substitua;
  • -r remove aspas de strings, então a saída pode conter quebras de linha ou caracteres significativos para o shell. Coloque dados não confiáveis entre aspas antes de canalizá-los adiante.

O que o /diff deste site faz e o que ele não faz

A ferramenta de comparação JSON embutida segue a mesma abordagem estrutural: os dois lados são analisados em árvores e alinhados recursivamente por caminho, produzindo path + type(add/remove/modify) + before/after. A ordem das chaves de objeto não afeta o resultado, e tudo roda no navegador — payloads colados nunca são enviados.

Seus arrays, porém, são alinhados por índice. Essa é uma troca deliberada, não um defeito:

  • Acrescentar no fim, modificar a mesma posição ou substituir por inteiro lhe dá uma saída limpa;
  • Inserir um registro no início ou no meio marca tudo depois dele como modify, e você precisa perceber que "na verdade só um registro é novo".

Então, quando você compara listas ordenadas, o movimento correto é ordenar os dois lados por id antes de colar. A entrada ordenada fixa o ponto de inserção e colapsa o ruído em uma entrada. Sem instalação necessária, e cobre a maioria dos casos do dia a dia.

Se você precisa de "gerar um patch aplicável" ou "detectar elementos movidos", a ferramenta da página não faz isso. Essa é a próxima seção.

Transformando um diff em patch: dois formatos, não os misture

Humanos leem diffs; programas aplicam patches. JSON tem dois formatos de patch com semânticas completamente diferentes.

Dimensão JSON Patch (RFC 6902) JSON Merge Patch (RFC 7396)
Forma Array de operações com op, path, value Um objeto JSON com a forma do documento alvo
Arrays Sim, por índice, /items/0, /- para acrescentar Não, arrays são substituídos por inteiro
Excluir uma chave remove Apenas via null, que não consegue excluir um campo cujo valor é legitimamente null
Expressar um move move / copy Não
Serve para Recursos contendo arrays, logs de auditoria, atualizações atômicas Atualizações parciais de objetos simples, entrega de configuração

O JSON Patch tem uma operação que as pessoas pulam: test.

[
  { "op": "test", "path": "/version", "value": 3 },
  { "op": "replace", "path": "/status", "value": "shipped" }
]

Se test falha, o patch inteiro é rejeitado. Isso é concorrência otimista embutida no protocolo — sem cabeçalho If-Match extra para garantir "só escrevi porque vi a versão 3". Para um endpoint PATCH isso vale mais do que qualquer convenção de documentação.

Os caminhos usam JSON Pointer (RFC 6901): ~0 escapa ~, ~1 escapa /. Chaves contendo barras (tipos MIME como chaves, por exemplo) precisam ser escapadas, ou o pointer cai em outro lugar. Os detalhes estão em JSON Pointer e Patch.

Resta um problema prático: patches gerados automaticamente a partir de um diff normalmente endereçam arrays por índice. Índices são frágeis em produção — uma inserção concorrente e /items/3 não aponta mais para o mesmo registro. Ou restrinja os patches gerados a campos de objeto, ou valide os campos críticos com test antes de aplicar.

Coloque a comparação no CI: asserte valores, não texto

Três regras bastam:

  1. Asserte valores analisados nos testes, nunca a saída de JSON.stringify. No Jest use toEqual; no Python assert a == b após json.loads. Ordem de chaves e indentação não participam.
  2. Coloque campos dinâmicos numa whitelist. requestId, timestamp, traceId diferem a cada execução; sem exclusões o teste fica permanentemente vermelho. propertyFilter no jsondiffpatch e exclude_paths no deepdiff existem para isso.
  3. Imprima caminhos em caso de falha, não o documento inteiro. Ninguém lê 5 MB de payload em um log; $.data.items[id=ORD-042].qty: 2 → 3 localiza o problema instantaneamente.

O desvio de configuração funciona do mesmo jeito: rode jq -S nas configurações de staging e produção, ordene arrays por uma chave de negócio, e o que restar é uma diferença real. Essa classe de problema — os mesmos dados parecendo diferentes entre ambientes — normalmente é um contrato indefinido, que é sobre o que trata projeto de contrato de API.

Perguntas frequentes

A ordem das chaves conta como diferença?

Não. A RFC 8259 define objetos como coleções não ordenadas, então {"a":1,"b":2} e {"b":2,"a":1} são o mesmo valor. Qualquer ferramenta que relata reordenação como mudança está fazendo comparação de texto, não comparação JSON.

Por que inserir um registro marca todas as entradas como alteradas?

Porque a ferramenta alinha arrays índice a índice. O novo elemento fica na posição 0, todo registro existente desliza um para baixo, e nenhuma posição coincide. Ordene ou indexe por uma chave como id antes de comparar em vez de comparar posicionalmente.

Ferramentas de comparação online enviam meus dados?

Depende da implementação. Ferramentas renderizadas no servidor enviam o payload a um backend, onde ele pode ser registrado; ferramentas do lado do cliente analisam no navegador e não enviam nada. Antes de comparar configs de produção ou payloads de usuários, confirme que a página calcula localmente, ou desconecte a rede e tente de novo.

O resultado de um diff pode ser usado diretamente como patch?

Não diretamente. Um diff diz onde as coisas diferem; o JSON Patch diz quais operações executar em ordem. Uma camada de conversão fica entre eles. Patches gerados também tendem a endereçar arrays por índice, o que quebra sob concorrência — adicione operações test antes de aplicar.

Como comparo arquivos de dezenas de megabytes?

Faça streaming primeiro (jq -c grava um registro por linha), ou extraia apenas a subárvore que lhe interessa, como jq '.data.items'. Uma comparação recursiva completa de dezenas de megabytes ainda cabe na memória; acima de cem megabytes, fragmente ou indexe por chave. Para números concretos de memória e tempo, veja desempenho de JSON grande.

Devo ordenar arrays por id antes de comparar?

Sim. A ferramenta de comparação JSON embutida alinha arrays por índice, então inserir um registro no início ou no meio marca tudo depois dele como modify; ordenar os dois lados por id antes de colar fixa o ponto de inserção e colapsa o ruído em uma entrada. A exceção é quando a ordem do array é ela mesma dado — etapas de migração, rankings de busca, pontos de gráfico — onde a comparação posicional está correta e ordenar está errado.