Usar jq o herramientas de línea de comandos alternativas para diferenciar archivos JSON


¿Hay alguna utilidad de línea de comandos que se pueda usar para encontrar si dos archivos JSON son idénticos con invariancia al orden de within-dictionary-key y within-list-element?

¿Se podría hacer esto con jq ¿o alguna otra herramienta equivalente?

Ejemplos:

Estos dos archivos JSON son idénticos

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

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

Pero estos dos archivos JSON son diferentes:

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

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

Eso sería:

$ some_diff_command A.json B.json

$ some_diff_command A.json C.json
The files are not structurally identical
 32
Author: peak, 2015-08-11

6 answers

Dado que la comparación de jq ya compara objetos sin tener en cuenta el orden de claves, todo lo que queda es ordenar todas las listas dentro del objeto antes de compararlas. Suponiendo que sus dos archivos se llaman a.json y b.json, en la última jq nightly:

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

Este programa debe devolver "true" o "false" dependiendo de si los objetos son iguales o no usando la definición de igualdad que usted pide.

EDITAR: La construcción (.. | arrays) |= sort en realidad no funciona como se espera en algún borde caso. Este problema de GitHub explica por qué y proporciona algunas alternativas, tales como:

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

Aplicado a la invocación jq anterior:

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'
 12
Author: UrsinusTheStrong,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-19 16:39:29

En principio, si tienes acceso a bash o a algún otro shell avanzado, podrías hacer algo como

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

Usando subprocesos. Esto formateará el json con claves ordenadas y una representación consistente de puntos flotantes. Esas son las únicas dos razones en las que puedo pensar por las que json con el mismo contenido se imprimiría de manera diferente. Por lo tanto, hacer una simple comparación de cadenas después dará como resultado una prueba adecuada. Probablemente también vale la pena señalar que si no puedes usar bash puede obtener los mismos resultados con archivos temporales, simplemente no es tan limpio.

Esto no responde completamente a tu pregunta, porque en la forma en que declaraste la pregunta querías ["John", "Bryan"] y ["Bryan", "John"] comparar idénticamente. Dado que json no tiene el concepto de un conjunto, solo una lista, estos deben considerarse distintos. El orden es importante para las listas. Tendría que escribir alguna comparación personalizada si quisiera que se compararan por igual, y para hacer eso tendría que definir lo que quiere decir con igualdad. ¿Importa el orden para todas las listas o solo para algunas? ¿Qué pasa con los elementos duplicados? Alternativamente, si desea que se representen como un conjunto, y los elementos son cadenas, puede colocarlos en objetos como {"John": null, "Bryan": null}. El orden no importará cuando se comparen los de igualdad.

Actualizar

De la discusión de comentarios: Si quieres tener una mejor idea de por qué el json no es el mismo, entonces

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

Producirá una salida más interpretable. vimdiff podría ser preferible a diff dependiendo de los gustos.

 37
Author: Erik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-17 20:26:47

Aquí hay una solución usando la función genérica 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);

Ejemplo:

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

Produce:

true

Y envuelto como una escritura 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 es una versión incorporada de jq > 1.5, y por lo tanto se puede omitir si su jq lo incluye, pero no hay ningún daño en incluirlo redundantemente en un script jq.

POST-POSTSCRIPT: La versión incorporada de walk se ha cambiado recientemente para que ya no ordene las claves dentro de un objeto. Específicamente, utiliza keys_unsorted. Para la tarea en cuestión, se debe usar la versión que usa keys.

 5
Author: peak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-17 11:41:49

Uso jd con la opción -set:

Ninguna salida significa ninguna diferencia.

$ jd -set A.json B.json

Las diferencias se muestran como @ path y + o -.

$ jd -set A.json C.json

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

Los diffs de salida también se pueden usar como archivos de parche con la opción -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

 4
Author: Joe Burnett,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-06 03:53:36

Si también quieres ver las diferencias, usa la respuesta de @Erik como inspiración y 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
Author: tokland,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-18 11:04:59

Tal vez podría usar esta herramienta de clasificación y comparación: http://novicelab.org/jsonsortdiff / que primero ordena los objetos semánticamente y luego los compara. Se basa en https://www.npmjs.com/package/jsonabc

 0
Author: Shivraj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-10-06 06:34:58