Verwenden von jq oder alternativen Befehlszeilentools zum Vergleichen von JSON-Dateien

78

Gibt es Befehlszeilenprogramme, mit denen ermittelt werden kann, ob zwei JSON-Dateien mit der Invarianz der Reihenfolge innerhalb des Wörterbuchschlüssels und innerhalb des Listenelements identisch sind?

Könnte dies mit jqoder einem anderen gleichwertigen Werkzeug durchgeführt werden?

Beispiele:

Diese beiden JSON-Dateien sind identisch

A::

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

B::

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

Diese beiden JSON-Dateien sind jedoch unterschiedlich:

A::

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

C::

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

Das wäre:

$ some_diff_command A.json B.json

$ some_diff_command A.json C.json
The files are not structurally identical
Amelio Vazquez-Reina
quelle

Antworten:

36

Da der Vergleich von jq bereits Objekte vergleicht, ohne die Schlüsselreihenfolge zu berücksichtigen, müssen nur alle Listen innerhalb des Objekts sortiert werden, bevor sie verglichen werden. Angenommen, Ihre beiden Dateien haben den Namen a.jsonund b.jsonspätestens abends:

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

Dieses Programm sollte "true" oder "false" zurückgeben, je nachdem, ob die Objekte unter Verwendung der von Ihnen angeforderten Definition der Gleichheit gleich sind oder nicht.

BEARBEITEN: Das (.. | arrays) |= sortKonstrukt funktioniert in einigen Randfällen nicht wie erwartet. Dieses GitHub-Problem erklärt, warum und bietet einige Alternativen, wie zum Beispiel:

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

Auf den obigen jq-Aufruf angewendet:

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'
UrsinusTheStrong
quelle
1
Ich habe versucht , zu ändern , --argfile a a.jsonum --arg a $aohne Glück (als $ aa json string). Irgendeine Idee, wie man sich Strings nähert, nicht Dateien?
Simon Ernesto Cardenas Zarate
@ SimonErnestoCardenasZarate Wenn Sie immer noch dieses Problem haben, möchten Sie möglicherweise stattdessen das --argjsonArgument
Brian
91

Wenn Sie Zugriff auf Bash oder eine andere erweiterte Shell haben, können Sie im Prinzip so etwas tun

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

unter Verwendung von Unterprozessen. Dadurch wird der JSON mit sortierten Schlüsseln und einer konsistenten Darstellung von Gleitkommazahlen formatiert. Dies sind die einzigen zwei Gründe, warum json mit demselben Inhalt unterschiedlich gedruckt wird. Wenn Sie anschließend einen einfachen Zeichenfolgenvergleich durchführen, erhalten Sie einen ordnungsgemäßen Test. Es ist wahrscheinlich auch erwähnenswert, dass wenn Sie Bash nicht verwenden können, Sie mit temporären Dateien dieselben Ergebnisse erzielen können, es einfach nicht so sauber ist.

Dies beantwortet Ihre Frage nicht ganz, da Sie die gewünschte Frage gestellt ["John", "Bryan"]und ["Bryan", "John"]identisch verglichen haben. Da json nicht das Konzept einer Menge hat, sondern nur eine Liste, sollten diese als unterschiedlich betrachtet werden. Reihenfolge ist wichtig für Listen. Sie müssten einen benutzerdefinierten Vergleich schreiben, wenn Sie möchten, dass sie gleichermaßen verglichen werden, und dazu müssten Sie definieren, was Sie unter Gleichheit verstehen. Ist die Reihenfolge für alle Listen oder nur für einige wichtig? Was ist mit doppelten Elementen? Wenn Sie möchten, dass sie als Menge dargestellt werden und die Elemente Zeichenfolgen sind, können Sie sie auch in Objekte wie setzen {"John": null, "Bryan": null}. Die Reihenfolge spielt beim Vergleich der Gleichheit keine Rolle.

Aktualisieren

Aus der Kommentardiskussion: Wenn Sie eine bessere Vorstellung davon bekommen möchten, warum der JSON nicht derselbe ist, dann

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

erzeugt eine besser interpretierbare Ausgabe. vimdiffJe nach Geschmack ist es möglicherweise vorzuziehen, sich zu unterscheiden.

Erik
quelle
1
Beachten Sie, dass dies Version 1.5 oder höher vonjq
Adam Baxter
1
@voltagex Aus dem Online-Handbuch ( stedolan.github.io/jq/manual/v1.4/#Invokingjq ) geht hervor, dass es tatsächlich in 1.4 hinzugefügt wurde, obwohl ich nicht weiß, ob jqes Argumente im Posix-Stil gibt muss anrufenjq -c -S ...
Erik
4
Eine sauberere, visuelle Form IMO istvimdiff <(jq -S . a.json) <(jq -S . b.json)
Ashwin Jayaprakash
1
Ja, Sie sollten das entfernen -c(was die Ausgabe kompakt macht). Stileinstellungen sind für Ihre Antwort nicht relevant.
Odinho - Velmont
@ odinho-Velmont @Ashwin Jayaprakash Es ist wahr, dass das cnicht unbedingt notwendig ist, aber für mich gibt es keinen Grund für cmp, identische Leerzeichen zu vergleichen, und keinen Grund für jq, sich die Mühe zu machen, es auszugeben. diff, vimdiffOder irgendein Werkzeug , das Dateivergleich tut funktioniert, aber cmpist alles , was notwendig ist.
Erik
19

Verwenden Sie jdmit der -setOption:

Keine Ausgabe bedeutet keinen Unterschied.

$ jd -set A.json B.json

Unterschiede werden als @ -Pfad und + oder - angezeigt.

$ jd -set A.json C.json

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

Die Ausgabedifferenzen können mit dieser -pOption auch als Patch-Dateien verwendet werden .

$ 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

Joe Burnett
quelle
So unterschätzt sollte es ein Vergehen sein. Gibt eine tatsächliche diffformatierungskompatible Ausgabe. Tolle.
Ijoseph
7

Es gibt eine Antwort auf diese hier , die nützlich sein würde.

Im Wesentlichen können Sie die Git- diffFunktionalität verwenden (auch für nicht von Git verfolgte Dateien), die auch Farbe in die Ausgabe einbezieht:

git diff --no-index payload_1.json payload_2.json

Maikon
quelle
2
Dies ist empfindlich gegenüber der Ordnung, die das OP ignorieren wollte
Andreas
6

Hier ist eine Lösung mit der generischen Funktion 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);

Beispiel:

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

produziert:

true

Und als Bash-Skript verpackt:

#!/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 ist eine integrierte Version von jq> 1.5 und kann daher weggelassen werden, wenn Ihr jq es enthält, aber es schadet nicht, es redundant in ein jq-Skript aufzunehmen.

POST-POSTSCRIPT: Die integrierte Version von walkwurde kürzlich geändert, sodass die Schlüssel innerhalb eines Objekts nicht mehr sortiert werden. Insbesondere verwendet es keys_unsorted. Für die jeweilige Aufgabe keyssollte die verwendete Version verwendet werden.

Gipfel
quelle
1
Vielen Dank für die Erwähnung, dass walkin jq 1.5 hinzugefügt wurde. Ich habe mir einen Kompromissoperator zwischen filterund gewünscht mapund es sieht so aus, als ob es das ist.
Noah Sussman
1

Ziehen Sie das Beste aus den beiden besten Antworten heraus, um ein jqbasiertes JSON-Diff zu erhalten:

diff \
  <(jq -S 'def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); (. | (post_recurse | arrays) |= sort)' "$original_json") \
  <(jq -S 'def post_recurse(f): def r: (f | select(. != null) | r), .; r; def post_recurse: post_recurse(.[]?); (. | (post_recurse | arrays) |= sort)' "$changed_json")

Dies erfordert die elegante Array-Sortierlösung von https://stackoverflow.com/a/31933234/538507 (mit der wir Arrays als Sets behandeln können) und die saubere Bash-Umleitung diffvon https://stackoverflow.com/a/37175540/ 538507 Dies betrifft den Fall, in dem Sie einen Unterschied von zwei JSON-Dateien wünschen und die Reihenfolge des Array-Inhalts nicht relevant ist.

Andrew
quelle
0

Wenn Sie auch die Unterschiede sehen möchten, verwenden Sie die Antwort von @ Erik als Inspiration und js-verschönern :

$ 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"
 }]
tokland
quelle
6
... oder weißt du, entferne das einfach -cvon der jqKommandozeile. Ich weiß nicht, lieber keine zusätzlichen unnötigen Werkzeuge einführen;)
Odinho - Velmont
0

Ein weiteres Tool für diejenigen, zu denen die vorherigen Antworten nicht passen, können Sie jdd ausprobieren .

Es basiert auf HTML, sodass Sie es entweder online unter www.jsondiff.com verwenden können. Wenn Sie es lieber lokal ausführen möchten , laden Sie einfach das Projekt herunter und öffnen Sie die Datei index.html.

Acapulco
quelle