web-dev-qa-db-fra.com

Utilisation de jq ou d’autres outils de ligne de commande pour comparer les fichiers JSON

Existe-t-il des utilitaires de ligne de commande permettant de déterminer si deux fichiers JSON sont identiques, avec invariance dans la commande de clé de dictionnaire et élément de liste?

Cela pourrait-il être fait avec jq ou un autre outil équivalent?

Exemples:

Ces deux fichiers JSON sont identiques

A:

{
  "People": ["John", "Bryan"],
  "City": "Boston",
  "State": "MA"
}

B:

{
  "People": ["Bryan", "John"],
  "State": "MA",
  "City": "Boston"
}

mais ces deux fichiers JSON sont différents:

A:

{
  "People": ["John", "Bryan", "Carla"],
  "City": "Boston",
  "State": "MA"
}

C:

{
  "People": ["Bryan", "John"],
  "State": "MA",
  "City": "Boston"
}

Ce serait:

$ some_diff_command A.json B.json

$ some_diff_command A.json C.json
The files are not structurally identical
42

Comme la comparaison de jq compare déjà des objets sans tenir compte de l'ordre des clés, il ne reste plus qu'à trier toutes les listes à l'intérieur de l'objet avant de les comparer. En supposant que vos deux fichiers s'appellent a.json et b.json, le dernier jq nocturne:

jq --argfile a a.json --argfile b b.json -n '($a | (.. | arrays) |= sort) as $a | ($b | (.. | arrays) |= sort) as $b | $a == $b'

Ce programme doit renvoyer "true" ou "false" selon que les objets sont égaux ou non en utilisant la définition d'égalité que vous demandez.

EDIT: La construction (.. | arrays) |= sort ne fonctionne pas comme prévu sur certains cas Edge. Ce numéro de GitHub explique pourquoi et propose des alternatives, telles que:

def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); (post_recurse | arrays) |= sort

Appliqué à l'invocation jq ci-dessus:

jq --argfile a a.json --argfile b b.json -n 'def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); ($a | (post_recurse | arrays) |= sort) as $a | ($b | (post_recurse | arrays) |= sort) as $b | $a == $b'
17
user3899165

En principe, si vous avez accès à bash ou à un autre shell avancé, vous pouvez faire quelque chose comme:

cmp <(jq -cS . A.json) <(jq -cS . B.json)

en utilisant des sous-processus. Cela formatera le JSON avec des clés triées et une représentation cohérente des points flottants. Ce sont les deux seules raisons auxquelles je peux penser pour lesquelles json avec le même contenu serait imprimé différemment. Par conséquent, faire une simple comparaison de chaîne par la suite donnera un test approprié. Il convient également de noter que si vous ne pouvez pas utiliser bash, vous pouvez obtenir les mêmes résultats avec des fichiers temporaires, ce n'est tout simplement pas aussi propre.

Cela ne répond pas tout à fait à votre question, car dans la manière dont vous avez posé la question, vous vouliez que ["John", "Bryan"] et ["Bryan", "John"] se comparent de manière identique. Puisque json n’a pas le concept d’un ensemble, mais seulement d’une liste, ceux-ci doivent être considérés comme distincts. L'ordre est important pour les listes. Vous auriez à écrire une comparaison personnalisée si vous voulez qu'ils comparent de manière égale, et pour ce faire, vous devrez définir ce que vous entendez par égalité. L'ordre est-il important pour toutes les listes ou seulement certaines? Qu'en est-il des éléments en double? Sinon, si vous souhaitez les représenter sous la forme d'un ensemble et que les éléments sont des chaînes, vous pouvez les placer dans des objets tels que {"John": null, "Bryan": null}. L'ordre n'aura pas d'importance lorsque l'on compare ceux de l'égalité.

Mettre à jour

De la discussion de commentaire: Si vous voulez avoir une meilleure idée de la raison pour laquelle le json n'est pas le même, alors

diff <(jq -S . A.json) <(jq -S . B.json)

produira une sortie plus interprétable. vimdiff pourrait être préférable à diff en fonction des goûts.

46
Erik

Voici une solution utilisant la fonction générique walk/1

# Apply f to composite entities recursively, and to atoms
def walk(f):
  . as $in
  | if type == "object" then
      reduce keys[] as $key
        ( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
  Elif type == "array" then map( walk(f) ) | f
  else f
  end;

def normalize: walk(if type == "array" then sort else . end);

# Test whether the input and argument are equivalent
# in the sense that ordering within lists is immaterial:
def equiv(x): normalize == (x | normalize);

Exemple:

{"a":[1,2,[3,4]]} | equiv( {"a": [[4,3], 2,1]} )

produit:

true

Et emballé comme un script bash:

#!/bin/bash

JQ=/usr/local/bin/jq
BN=$(basename $0)

function help {
  cat <<EOF

Syntax: $0 file1 file2

The two files are assumed each to contain one JSON entity.  This
script reports whether the two entities are equivalent in the sense
that their normalized values are equal, where normalization of all
component arrays is achieved by recursively sorting them, innermost first.

This script assumes that the jq of interest is $JQ if it exists and
otherwise that it is on the PATH.

EOF
  exit
}

if [ ! -x "$JQ" ] ; then JQ=jq ; fi

function die     { echo "$BN: $@" >&2 ; exit 1 ; }

if [ $# != 2 -o "$1" = -h  -o "$1" = --help ] ; then help ; exit ; fi

test -f "$1" || die "unable to find $1"
test -f "$2" || die "unable to find $2"

$JQ -r -n --argfile A "$1" --argfile B "$2" -f <(cat<<"EOF"
# Apply f to composite entities recursively, and to atoms
def walk(f):
  . as $in
  | if type == "object" then
      reduce keys[] as $key
        ( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
  Elif type == "array" then map( walk(f) ) | f
  else f
  end;

def normalize: walk(if type == "array" then sort else . end);

# Test whether the input and argument are equivalent
# in the sense that ordering within lists is immaterial:
def equiv(x): normalize == (x | normalize);

if $A | equiv($B) then empty else "\($A) is not equivalent to \($B)" end

EOF
)

POSTSCRIPT: walk/1 est une version intégrée de jq> 1.5 et peut donc être omise si votre jq l'inclut, mais il n'y a aucun mal à l'inclure de manière redondante dans un script jq.

POST-POSTSCRIPT: La version intégrée de walk a récemment été modifiée pour ne plus trier les clés d'un objet. Plus précisément, il utilise keys_unsorted. Pour la tâche à accomplir, la version utilisant keys doit être utilisée.

6
peak

Utilisez jd avec l’option -set:

Pas de sortie signifie pas de différence.

$ jd -set A.json B.json

Les différences sont représentées par un @ chemin et + ou -.

$ jd -set A.json C.json

@ ["People",{}]
+ "Carla"

Les diffs de sortie peuvent également être utilisés comme fichiers de correctifs avec l'option -p.

$ jd -set -o patch A.json C.json; jd -set -p patch B.json

{"City":"Boston","People":["John","Carla","Bryan"],"State":"MA"}

https://github.com/josephburnett/jd#command-line-usage

6
Joe Burnett

Vous pourriez peut-être utiliser cet outil de tri et de différenciation: http://novicelab.org/jsonsortdiff/ qui trie d’abord les objets sémantiquement puis les compare. Il est basé sur https://www.npmjs.com/package/jsonabc

1
Shivraj

Si vous voulez aussi voir les différences, utilisez la réponse de @ Erik comme source d'inspiration et js-beautify :

$ echo '[{"name": "John", "age": 56}, {"name": "Mary", "age": 67}]' > file1.json
$ echo '[{"age": 56, "name": "John"}, {"name": "Mary", "age": 61}]' > file2.json

$ diff -u --color \
        <(jq -cS . file1.json | js-beautify -f -) \
        <(jq -cS . file2.json | js-beautify -f -)
--- /dev/fd/63  2016-10-18 13:03:59.397451598 +0200
+++ /dev/fd/62  2016-10-18 13:03:59.397451598 +0200
@@ -2,6 +2,6 @@
     "age": 56,
     "name": "John Smith"
 }, {
-    "age": 67,
+    "age": 61,
     "name": "Mary Stuart"
 }]
0
tokland