Comparar duas estruturas JSON

compara duas estruturas JSON e lista as adições, supressões e modificações, chave a chave

Porquê comparar dois JSON?

Comparar duas estruturas JSON aparece regularmente na vida de um programador. Uma resposta de API que muda depois de uma atualização. Um ficheiro de configuração que diverge entre dois ambientes. Uma exportação de objetos que tem de se alinhar com uma referência. Um diff JSON responde precisamente a esta questão: o que foi adicionado, removido ou modificado, e em que local?

Um diff linha a linha (à la git diff) muitas vezes não chega. Se a formatação difere (espaços, ordem das chaves), o diff textual sinaliza centenas de linhas embora a estrutura de dados seja idêntica. Um comparador JSON trabalha na estrutura uma vez analisada, o que elimina esse ruído e revela apenas as diferenças semânticas.

O formato do diff produzido pela ferramenta

Para cada diferença, a ferramenta devolve:

  • um caminho em formato JSONPath simplificado, por exemplo $.user.address[0].city;
  • um tipo entre added (adicionado à direita), removed (presente à esquerda apenas), modified (valores diferentes);
  • o valor à esquerda e/ou o valor à direita conforme o tipo.

Um diff vazio significa que os dois JSON são estruturalmente idênticos, independentemente da sua formatação ou da ordem das chaves.

Como o algoritmo percorre as duas estruturas

O algoritmo é recursivo. Em cada nível, identifica o tipo dos dois valores comparados:

  • Se ambos forem objetos associativos, faz a união das chaves. Para cada chave, desce recursivamente, ou marca added/removed se a chave só existir num lado.
  • Se ambos forem arrays ordenados, compara posição a posição. Uma diferença no início do array pode desalinhar todo o resto, o que produz um diff verboso: é uma limitação assumida do diff estrutural simples.
  • Se os tipos diferem (objeto contra array, escalar contra null), é marcado modified.
  • Se os dois valores são escalares (string, número, booleano, null), uma simples comparação estrita basta.

A ordem das chaves num objeto não conta: {"a": 1, "b": 2} e {"b": 2, "a": 1} produzem um diff vazio. Está conforme à semântica JSON, onde a ordem não é significativa. Em contrapartida, a ordem dos elementos de um array conta: um array é ordenado por construção.

Um exemplo concreto

Eis duas versões de um objeto de utilizador:

// gauche
{
  "id": 42,
  "name": "Alice",
  "roles": ["admin", "editor"]
}

// droite
{
  "id": 42,
  "name": "Alice Martin",
  "roles": ["admin", "viewer"],
  "active": true
}

O diff produzido:

  • $.name: modified, "Alice" -> "Alice Martin"
  • $.roles[1]: modified, "editor" -> "viewer"
  • $.active: added, true

Casos de uso

  • Diff de ambientes: comparar a saída de um endpoint em pré-produção e em produção. Muito útil durante uma migração ou um refresh de cache.
  • Auditoria de migração: comparar uma exportação antes e depois de uma transformação para verificar que nenhum campo se perdeu.
  • Regressão de API: antes e depois de uma modificação, comparar a resposta para um mesmo pedido. Um diff vazio confirma a não-regressão.
  • Sincronização de configurações: comparar composer.json entre dois ramos, dois ficheiros .eslintrc, duas configurações Symfony.
  • Snapshot tests: substituir uma comparação linha a linha por uma comparação estrutural numa suite de testes de integração.

Limites do diff estrutural

Comparar arrays ordenados é uma limitação conhecida dos diffs estruturais simples. Se inserirmos um elemento no início do array, todas as posições seguintes desalinham-se e o diff sinaliza cada diferença como uma modificação. Para tais casos, existem algoritmos mais avançados (Myers, Patience, diff por chave natural), mas saem do enquadramento de uma ferramenta de comparação generalista.

O diff também não diz porquê houve uma mudança: é uma constatação. Para analisar uma regressão, é preciso cruzar essa constatação com os commits, as implantações e os parâmetros do pedido.

JSON diff vs JSON Patch (RFC 6902)

Um formato complementar é o JSON Patch (RFC 6902). Descreve, sob forma de operações (add, remove, replace, move, copy, test), como transformar um documento JSON noutro. Enquanto o nosso diff é uma constatação (humana), o JSON Patch é uma receita (máquina). As duas representações são equivalentes para os casos simples, e o JSON Patch é útil para APIs RESTful que aceitam modificações parciais.

Perguntas frequentes

O diff depende da ordem das chaves?

Não: para um objeto JSON, a ordem não é significativa. O comparador produz o mesmo resultado quer as chaves estejam ordenadas ou não.

Como gerir arrays cuja ordem não é importante?

A ferramenta considera por defeito os arrays como ordenados (é a semântica JSON). Se tratar conjuntos, ordene os dois arrays por uma chave natural antes de comparar, ou utilize um serviço de comparação especializado que tenha em conta essa semântica.

Qual a diferença com um diff Git?

O Git compara linhas de texto. Se a indentação ou a ordem das chaves diferem, o diff Git fica muito verboso. O diff JSON trabalha na estrutura analisada e sinaliza apenas as diferenças de dados.

Um JSON inválido é aceite?

Não: se um dos dois JSON não fizer parse, a ferramenta devolve um erro. Valide primeiro com o nosso validador JSON.

Exemplo de pedido

curl -X POST https://cdrn.fr/api/v1/tools/json-diff/execute \
  -H "Content-Type: application/json" \
  -d '{"left":"...","right":"..."}'

Esquema de entrada

Campo Tipo Obrigatório Predefinição
left text
right text

Pontos de acesso

  • GET https://cdrn.fr/api/v1/tools - lista todas as ferramentas disponíveis
  • GET https://cdrn.fr/api/v1/tools/json-diff - obtém o esquema desta ferramenta
  • POST https://cdrn.fr/api/v1/tools/json-diff/execute - executa esta ferramenta com um payload JSON