Diff de JSON

Compare dois documentos JSON e veja o que foi adicionado, removido ou alterado. Ordem das chaves, indentação e minificação não contam; nada sai do navegador.

Primeiro JSON
Segundo JSON
Resultado

Cole JSON nos dois painéis para compará-los

Comparar os dados, e não o texto

Uma comparação linha a linha responde a uma pergunta sobre a escrita: quais linhas deste arquivo não são linhas daquele. É a pergunta certa para um texto corrido e para código-fonte, e a errada para JSON, porque um documento JSON não tem uma única maneira correta de ser escrito. Reordene os membros de um objeto, mude a indentação ou minifique um dos lados, e todas as linhas mudam enquanto os dados não se movem nem um pouco. Esta ferramenta compara aquilo em que os dois documentos são convertidos ao serem analisados, então o que ela relata é o que de fato mudou. Abaixo está um mesmo documento escrito duas vezes: minificado na primeira linha e depois formatado, com os membros em outra ordem.

{"name":"checkout","port":8080,"tags":["a","b"]}

{
  "port": 8080,
  "tags": ["a", "b"],
  "name": "checkout"
}

Esses dois não têm uma única linha em comum, e esta ferramenta não relata nenhuma diferença entre eles: os mesmos membros, os mesmos valores, a mesma ordem dentro da lista. A consequência prática é que você nunca precisa reformatar um documento para a comparação funcionar — o que significaria mudar justamente aquilo que você se propôs a comparar.

O que nunca conta como diferença

Quatro coisas sobre como um documento está escrito são ignoradas, e são ignoradas sem condição alguma. Não existe controle que ligue qualquer uma delas de volta, porque uma ferramenta capaz disso seriam duas ferramentas com dois significados a explicar, e quem lê teria de saber qual das duas produziu a resposta à sua frente.

  • A ordem em que os membros de um objeto estão escritos. {"a": 1, "b": 2} e {"b": 2, "a": 1} carregam os mesmos dados, então um serializador que emite as chaves em outra ordem a cada execução nunca aparece aqui como mudança.
  • Espaço em branco de qualquer tipo — a indentação, as quebras de linha, o espaço depois dos dois-pontos. Um documento minificado e um formatado carregando os mesmos dados são o mesmo documento duas vezes.
  • Como um número está escrito. 1e3 é 1000 e 1.50 é 1.5, porque um número JSON tem um valor e não uma grafia: dois documentos que diferem só nisso carregam os mesmos números, e nada é relatado a respeito deles.
  • Como um caractere dentro de uma string está escapado. Os seis caracteres \u0041 e a letra A são a mesma string de um caractere.

Duas coisas que parecem pertencer a essa lista estão deliberadamente fora dela. A ordem dos elementos de uma lista é dado — [1, 2] e [2, 1] são documentos diferentes — e um membro cujo valor é null não é o mesmo que um membro que não está ali, então um contra o outro é relatado em vez de passar em silêncio.

Adicionado, removido, alterado — e uma mudança de tipo

Cada linha do resultado é exatamente de um entre três tipos, e juntos eles cobrem tudo o que a comparação pode encontrar. Tome estes dois documentos:

{"host":"api.example.com","port":8080,"debug":true}

{"host":"api.example.com","port":"8080","retries":3}
  • Alterado em $['port']: o membro está nos dois documentos e o valor dele não é o mesmo, então os dois valores aparecem. Este também é marcado como mudança de tipo, porque 8080 é um número e "8080" é uma string.
  • Removido em $['debug']: o membro está no primeiro documento e não está no segundo.
  • Adicionado em $['retries']: o membro está no segundo documento e não está no primeiro.

O membro chamado host está nos dois com o mesmo valor, então não é relatado de jeito nenhum. Uma mudança de tipo é uma propriedade de uma alteração e não um quarto tipo de linha: é o desvio que um esquema percebe e que uma pessoa lendo dois arquivos normalmente não percebe, então ele é apontado onde acontece em vez de ficar por sua conta.

Onde uma diferença fica, escrito como um caminho

Cada linha nomeia o lugar em que está, e o nomeia como um caminho dentro do documento em vez de como um número de linha — um número de linha seria um dado sobre a escrita, que é justamente o que esta ferramenta acabou de ignorar. Os caminhos são caminhos normalizados da RFC 9535, a única grafia que este site usa sempre que uma posição dentro de JSON precisa ser anotada.

  • $ é o documento inteiro, $['port'] é o membro chamado port que está nele, e $['hosts'][1] é o elemento no índice 1 da lista sob hosts. Um membro é sempre escrito entre colchetes e aspas, então um nome que contenha um ponto, um espaço ou um colchete próprio não precisa de nada especial.
  • Um caminho é uma consulta que você pode rodar. Cole-o no Testador de JSONPath deste site junto com o documento a que ele pertence e ele seleciona exatamente aquele nó, que é a maneira mais rápida de ler uma diferença no meio em que ela está.
  • É uma consulta contra o documento em que o nó realmente está: o caminho de um membro adicionado roda sobre o segundo documento e o de um membro removido sobre o primeiro. Nada aqui nomeia algum dia um nó que não existe.

Uma linha alterada pode carregar dois caminhos, e isso é o casamento da próxima seção aparecendo: assim que uma lista ganha ou perde um elemento, o mesmo elemento passa a ficar em um índice diferente de cada lado. Quando os dois caminhos são iguais — o que é quase sempre — só um deles aparece.

Como duas listas são casadas uma com a outra

Dois objetos são comparados membro a membro, o que é simples porque um membro tem nome. Duas listas não têm nomes, só posições, e compará-las posição por posição é o que torna ilegível a maior parte do que uma comparação produz: insira um registro no começo de uma lista longa e nada mais está onde estava, então uma comparação que lê posição como identidade relata a lista inteira como diferente — verdade num sentido bem estreito e inútil em todos os outros. Por isso os elementos são casados primeiro, pelo que eles são e não por um campo que você indique, e só o que o casamento deixa de fora é relatado.

{"hosts": ["alpha", "beta", "gamma"]}

{"hosts": ["alpha", "staging", "beta", "gamma"]}

Isso é uma linha só: staging foi adicionado em $['hosts'][1]. Os elementos chamados beta e gamma andaram uma posição e não são mencionados, porque mover não é mudar. Esse mesmo casamento é o que permite que um único campo editado dentro de um item de uma lista seja relatado como uma alteração naquele campo, em vez de como o item inteiro tendo sido trocado por outro.

A ordem continua contando, e o casamento não finge o contrário: [1, 2] contra [2, 1] volta como um 1 removido do começo e um 1 adicionado depois do 2, que é o que aconteceu. Duas listas também podem estar diferentes demais para que casá-las valha o custo, e passado esse ponto a comparação recorre a ir posição por posição e diz isso no topo do resultado — assim uma resposta barulhenta fica explicada em vez de misteriosa.

O relatório e a árvore

A mesma comparação é oferecida em duas apresentações, e alternar entre elas não recalcula nada: a árvore é desenhada a partir da própria lista de diferenças que o relatório enumera, então as duas não podem discordar sobre o que mudou nem sobre quanto há.

  • O relatório responde ao que mudou: uma linha por diferença, cada uma carregando o caminho, o tipo e o valor de cada lado. É a visão para percorrer quando você quer a lista inteira à sua frente.
  • A árvore responde a onde no documento: os dois documentos fundidos num só esquema, com abertas apenas as partes que carregam uma diferença. Cada trecho de filhos seguidos que não carregam nada se dobra em uma linha dizendo por quantos ela responde — o par da seção anterior é desenhado como cinco linhas: o documento, a lista dentro dele, uma linha dobrada valendo por um elemento, o elemento adicionado e uma linha dobrada valendo por dois.
  • A contagem ao lado do título do resultado são todas as diferenças que existem. Quando a lista na tela é cortada por comprimento essa contagem não se move, e um controle no pé da lista mostra o resto.
~! $['port'] 8080 -> "8080"
- $['debug'] true
+ $['retries'] 3

É isso que o controle de copiar produz para os dois documentos da terceira seção: um sinal para o tipo, depois o caminho, depois os valores com uma seta entre eles — um mais para um membro adicionado, um menos para um removido, um til para uma alteração e um til com ponto de exclamação onde o tipo também mudou. Está escrito em símbolos e não em palavras de propósito, para que um resultado colado seja o mesmo texto em qualquer dos idiomas do site em que você estivesse lendo, e ele não encurta nada: um valor que a tela cortou está inteiro ali dentro.

O que a análise custa, e o que a ferramenta diz em voz alta

Comparar aquilo em que dois documentos são convertidos ao serem analisados significa conviver com o que essa análise custa, e analisar JSON não é uma operação sem perdas. Isso pesa aqui mais do que em qualquer outro lugar deste site, porque a resposta que uma ferramenta de comparação menos pode se dar ao luxo de dar é «nenhuma diferença» entre dois documentos que de fato diferem. Por isso o texto de cada painel é lido uma segunda vez como texto, e o que a análise deixou cair é relatado ao lado da comparação em vez de ser engolido.

  • Um nome de membro escrito duas vezes no mesmo objeto. Só a cópia posterior sobrevive à análise, então só a cópia posterior chegou a ser comparada e a anterior não foi vista por ninguém.
  • Um número comprido demais para sobreviver como um double. Dois literais diferentes podem ser analisados até o mesmo valor, e identificadores são exatamente onde isso acontece: 12345678901234567890 e 12345678901234567891 são inteiros diferentes e um só número analisado, então a comparação sozinha chamaria de idênticos dois documentos cujos identificadores diferem.
{"user": {"id": 1}, "user": {"id": 2}}

Aquele documento é analisado até {"user": {"id": 2}}, e a ferramenta diz isso sob o painel em que ele foi colado, com a linha e a coluna do primeiro caso e quantos há no total. Nenhuma das duas perdas interrompe a comparação; as duas ficam ao lado dela. Um documento que não é analisado de jeito nenhum é relatado no mesmo lugar, com a posição do problema sempre que houver uma a dar, e os dois painéis são lidos de forma independente — assim dois documentos quebrados produzem dois erros de uma vez, em vez de um depois do outro. Um documento aninhado muito mais fundo do que um valor consegue ser desenhado é recusado de saída, o que é uma recusa e não uma aba travada.

Por que nada do que você cola sai desta aba

Os dois documentos ficam no seu navegador. Não há envio, não há requisição e não há servidor que pudesse guardar uma cópia: a comparação é aritmética rodando dentro da página que você já carregou, o que também é a razão de ela continuar funcionando com a rede desligada. É isso que torna seguro colar aqui uma resposta de produção, o registro de um cliente ou uma carga assinada.

Nada é guardado tampouco. Recarregue a página e os dois painéis estão vazios; não há histórico, não há conta e não há nada para apagar depois. A única coisa que sai algum dia é o que você mesmo copiar.

Perguntas frequentes

Isto ignora a ordem das chaves?
Sim, sempre, e essa é a razão de a ferramenta existir. Dois objetos com os mesmos membros e os mesmos valores são idênticos seja como for que tenham sido escritos, então um serializador que emite as chaves em outra ordem a cada execução nunca aparece como uma mudança. A indentação, a minificação, como um número está escrito e como um caractere está escapado são ignorados pela mesma razão, e nada disso é uma opção que você pudesse ter deixado do outro jeito.
Um membro com valor null é o mesmo que um membro que não está ali?
Não, e não existe opção para tratar os dois como iguais. {"note": null} contra {} é relatado como $['note'] removido. Um campo presente e vazio é uma afirmação diferente de um campo que ninguém enviou — toda linguagem de esquema e toda linguagem de tipos estáticos separa os dois — então juntá-los aqui jogaria fora algo que os documentos estão realmente dizendo.
[1, 2] e [2, 1] são relatados como iguais?
Não. Um array JSON é ordenado, então duas listas com os mesmos elementos em outra ordem são dois documentos diferentes, e chamá-los de iguais seria esta ferramenta dizendo algo falso sobre o formato. O que você recebe é uma remoção e uma adição: o 1 foi removido do começo e adicionado de novo depois do 2. Com objetos é o contrário, porque os membros deles são nomeados e não posicionados.
O que o caminho de cada linha quer dizer?
É onde a diferença fica dentro do documento, escrito como um caminho normalizado da RFC 9535: $ é o próprio documento, $['port'] um membro dele, $['hosts'][1] o elemento no índice 1 da lista sob hosts. Também é uma consulta que você pode rodar: cole-a no Testador de JSONPath deste site junto com o documento a que ela pertence e ela seleciona exatamente aquele nó, e nenhum outro.
Por que uma linha mostrou dois caminhos diferentes?
Porque casar duas listas pode deixar o mesmo elemento em um índice diferente de cada lado. Compare ["alpha", "beta"] com ["intro", "alpha", "beta!"] e saem duas linhas: intro foi adicionado em $[0], e beta virou beta! — que é $[1] no primeiro documento e $[2] no segundo. Quando os dois caminhos são iguais, e em geral são, só um deles aparece.
O que uma mudança de tipo quer dizer numa linha?
Que o valor não é apenas diferente, mas outro tipo de coisa: um número que virou string, um valor que virou null, um objeto que virou lista. Uma porta escrita 8080 de um lado e "8080" do outro é o caso mais comum, e em geral quer dizer que mudou um serializador ou um cliente, e não os dados. Isso é marcado na linha porque um valor editado e um contrato que se desvia pedem de você respostas diferentes.
Meus dois documentos parecem idênticos, mas um escreve a mesma chave duas vezes. O que acontece?
A análise fica com a cópia posterior e larga a anterior, então a comparação vê um único membro e bem pode não relatar diferença nenhuma. A ferramenta lê o texto bruto separadamente justamente por isso e diz, sob aquele painel, que um nome está escrito duas vezes, quantas vezes no total e onde fica o primeiro. Essa mesma segunda leitura pega um inteiro comprido demais para sobreviver como um double, que é a outra maneira pela qual dois documentos que diferem podem ser analisados em dois que não diferem.
Dá para eu receber um patch que eu possa aplicar?
Não, e isso é uma decisão, não algo faltando. Um JSON Patch é aplicado uma operação depois da outra, então os índices dentro dele se deslocam enquanto ele roda; emitir um é uma promessa sobre um programa que você rodaria contra os seus próprios dados, o que é uma afirmação bem maior do que descrever o que difere. O que você pode levar embora, em vez disso, é o relatório como texto, pelo controle de copiar — o mesmo em todos os idiomas do site, e sem encurtar em lugar nenhum.
Alguma coisa que eu colo é enviada para um servidor?
Não. Os dois documentos são comparados no seu navegador e nenhum deles sai dali: sem envio, sem requisição e sem nada guardado entre visitas. É isso que torna seguro colar aqui uma resposta de produção, e é por isso que a ferramenta continua funcionando com a rede desligada. Recarregue a página e os dois painéis ficam vazios de novo.

Ferramentas relacionadas

  • Diff de texto

    Quando a mudança que você quer ver está na escrita e não nos dados — uma passada de indentação, uma chave que mudou de lugar, ou dois arquivos que nem são JSON — a comparação que mostra isso é a de linha a linha. É justamente o que aquela página faz, com opções de espaços e maiúsculas e um patch copiável.

  • Formatador JSON

    Valida e embeleza JSON, com localizações de erro claras.

  • Formatador SQL

    Formata e embeleza SQL — vários dialetos.

  • Formatador XML

    Formata XML e verifica se está bem formado — embeleze ou minifique.