¿Cómo comparar dos objetos JSON con los mismos elementos en un orden diferente igual?

101

¿Cómo puedo probar si dos objetos JSON son iguales en Python, sin tener en cuenta el orden de las listas?

Por ejemplo ...

Documento JSON a :

{
    "errors": [
        {"error": "invalid", "field": "email"},
        {"error": "required", "field": "name"}
    ],
    "success": false
}

Documento JSON b :

{
    "success": false,
    "errors": [
        {"error": "required", "field": "name"},
        {"error": "invalid", "field": "email"}
    ]
}

ay bdeben comparar iguales, aunque el orden de las "errors"listas sea diferente.

Petter Friberg
fuente
2
Duplicado de stackoverflow.com/questions/11141644/…
user2085282
1
¿Por qué no simplemente decodificarlos y compararlos? ¿O quiere decir que el orden de la "Matriz" o los listelementos tampoco importa?
mgilson
@ user2085282 Esa pregunta tiene un problema diferente.
user193661
2
Por favor, perdona mi ingenuidad, pero ¿por qué? Los elementos de la lista tienen un orden específico por una razón.
ATOzTOA
1
Como se señaló en esta respuesta, una matriz JSON se ordena para que estos objetos que contienen matrices con diferentes órdenes de clasificación no sean iguales en sentido estricto. stackoverflow.com/a/7214312/18891
Eric Ness

Respuestas:

143

Si desea que dos objetos con los mismos elementos pero en un orden diferente se comparen iguales, lo obvio es comparar copias ordenadas de ellos, por ejemplo, para los diccionarios representados por sus cadenas JSON ay b:

import json

a = json.loads("""
{
    "errors": [
        {"error": "invalid", "field": "email"},
        {"error": "required", "field": "name"}
    ],
    "success": false
}
""")

b = json.loads("""
{
    "success": false,
    "errors": [
        {"error": "required", "field": "name"},
        {"error": "invalid", "field": "email"}
    ]
}
""")
>>> sorted(a.items()) == sorted(b.items())
False

... pero eso no funciona, porque en cada caso, el "errors"elemento del dict de nivel superior es una lista con los mismos elementos en un orden diferente, y sorted()no intenta ordenar nada excepto el nivel "superior" de un iterable.

Para solucionarlo, podemos definir una orderedfunción que ordenará de forma recursiva cualquier lista que encuentre (y convertirá los diccionarios en listas de (key, value)pares para que se puedan ordenar):

def ordered(obj):
    if isinstance(obj, dict):
        return sorted((k, ordered(v)) for k, v in obj.items())
    if isinstance(obj, list):
        return sorted(ordered(x) for x in obj)
    else:
        return obj

Si aplicamos esta función a ay b, los resultados son iguales:

>>> ordered(a) == ordered(b)
True
Zero Piraeus
fuente
1
muchas gracias Zero Piraeus. es exactamente la solución general que necesito. pero el único problema es que el código solo funciona para python 2.x no para python3. Recibo el siguiente error: TypeError: tipos no ordenados: dict () <dict () De todos modos, la solución ahora es clara. Intentaré que funcione para python3. Muchas gracias
1
@HoussamHsm Tenía la intención de arreglar esto para que funcionara con Python 3.x cuando mencionaste por primera vez el problema de dictados no ordenables, pero de alguna manera se me escapó. Ahora funciona tanto en 2.xy 3.x :-)
Zero Piraeus
cuando hay una lista como ['astr', {'adict': 'something'}], obtuve TypeErroral intentar ordenarlos.
Zhenxiao Hao
1
@ Blairg23 ha entendido mal la pregunta, que se trata de comparar objetos JSON como iguales cuando contienen listas cuyos elementos son iguales, pero en un orden diferente, no sobre un supuesto orden de diccionarios.
Zero Piraeus
1
@ Blairg23 Estoy de acuerdo en que la pregunta podría escribirse más claramente (aunque si miras el historial de edición , es mejor de lo que comenzó). Re: diccionarios y orden - sí, lo sé ;-)
Zero Piraeus
45

Otra forma podría ser usar la json.dumps(X, sort_keys=True)opción:

import json
a, b = json.dumps(a, sort_keys=True), json.dumps(b, sort_keys=True)
a == b # a normal string comparison

Esto funciona para diccionarios y listas anidados.

stpk
fuente
{"error":"a"}, {"error":"b"}vs {"error":"b"}, {"error":"a"} no podrá clasificar el último caso en el primer caso
ChromeHearts
@ Blairg23, pero ¿qué harías si tuvieras listas anidadas en el dictado? No se puede simplemente comparar el dictamen de nivel superior y terminarlo, no se trata de esto.
stpk
4
Esto no funciona si tiene listas adentro. por ejemplo json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil
7
@Danil y probablemente no debería. Las listas son una estructura ordenada y si difieren solo en orden, debemos considerarlas diferentes. Tal vez para su caso de uso, el orden no importe, pero no deberíamos asumir eso.
stpk
debido a que las listas están ordenadas por índice, no se repetirán. [0, 1] no debería ser igual a [1, 0] en la mayoría de situaciones. Así que esta es una buena solución para el caso normal, pero no para la pregunta anterior. todavía +1
Harrison
18

Decodifíquelos y compárelos como comentario mgilson.

El orden no importa para el diccionario siempre que las claves y los valores coincidan. (El diccionario no tiene orden en Python)

>>> {'a': 1, 'b': 2} == {'b': 2, 'a': 1}
True

Pero el orden es importante en la lista; ordenar resolverá el problema de las listas.

>>> [1, 2] == [2, 1]
False
>>> [1, 2] == sorted([2, 1])
True

>>> a = '{"errors": [{"error": "invalid", "field": "email"}, {"error": "required", "field": "name"}], "success": false}'
>>> b = '{"errors": [{"error": "required", "field": "name"}, {"error": "invalid", "field": "email"}], "success": false}'
>>> a, b = json.loads(a), json.loads(b)
>>> a['errors'].sort()
>>> b['errors'].sort()
>>> a == b
True

El ejemplo anterior funcionará para JSON en la pregunta. Para una solución general, consulte la respuesta de Zero Piraeus.

falsetru
fuente
2

Para los siguientes dos dicts 'dictWithListsInValue' y 'reorderedDictWithReorderedListsInValue', que son simplemente versiones reordenadas entre sí

dictObj = {"foo": "bar", "john": "doe"}
reorderedDictObj = {"john": "doe", "foo": "bar"}
dictObj2 = {"abc": "def"}
dictWithListsInValue = {'A': [{'X': [dictObj2, dictObj]}, {'Y': 2}], 'B': dictObj2}
reorderedDictWithReorderedListsInValue = {'B': dictObj2, 'A': [{'Y': 2}, {'X': [reorderedDictObj, dictObj2]}]}
a = {"L": "M", "N": dictWithListsInValue}
b = {"L": "M", "N": reorderedDictWithReorderedListsInValue}

print(sorted(a.items()) == sorted(b.items()))  # gives false

me dio un resultado incorrecto, es decir, falso.

Así que creé mi propio ObjectComparator cutstom así:

def my_list_cmp(list1, list2):
    if (list1.__len__() != list2.__len__()):
        return False

    for l in list1:
        found = False
        for m in list2:
            res = my_obj_cmp(l, m)
            if (res):
                found = True
                break

        if (not found):
            return False

    return True


def my_obj_cmp(obj1, obj2):
    if isinstance(obj1, list):
        if (not isinstance(obj2, list)):
            return False
        return my_list_cmp(obj1, obj2)
    elif (isinstance(obj1, dict)):
        if (not isinstance(obj2, dict)):
            return False
        exp = set(obj2.keys()) == set(obj1.keys())
        if (not exp):
            # print(obj1.keys(), obj2.keys())
            return False
        for k in obj1.keys():
            val1 = obj1.get(k)
            val2 = obj2.get(k)
            if isinstance(val1, list):
                if (not my_list_cmp(val1, val2)):
                    return False
            elif isinstance(val1, dict):
                if (not my_obj_cmp(val1, val2)):
                    return False
            else:
                if val2 != val1:
                    return False
    else:
        return obj1 == obj2

    return True


dictObj = {"foo": "bar", "john": "doe"}
reorderedDictObj = {"john": "doe", "foo": "bar"}
dictObj2 = {"abc": "def"}
dictWithListsInValue = {'A': [{'X': [dictObj2, dictObj]}, {'Y': 2}], 'B': dictObj2}
reorderedDictWithReorderedListsInValue = {'B': dictObj2, 'A': [{'Y': 2}, {'X': [reorderedDictObj, dictObj2]}]}
a = {"L": "M", "N": dictWithListsInValue}
b = {"L": "M", "N": reorderedDictWithReorderedListsInValue}

print(my_obj_cmp(a, b))  # gives true

lo que me dio el resultado esperado correcto!

La lógica es bastante simple:

Si los objetos son del tipo 'lista', compare cada elemento de la primera lista con los elementos de la segunda lista hasta que los encuentre, y si el elemento no se encuentra después de pasar por la segunda lista, entonces 'encontrado' sería = falso. se devuelve el valor 'encontrado'

De lo contrario, si los objetos a comparar son de tipo 'dict', compare los valores presentes para todas las claves respectivas en ambos objetos. (Se realiza una comparación recursiva)

De lo contrario, simplemente llame a obj1 == obj2. De forma predeterminada, funciona bien para el objeto de cadenas y números y para esos eq () se define adecuadamente.

(Tenga en cuenta que el algoritmo se puede mejorar aún más eliminando los elementos que se encuentran en object2, de modo que el siguiente elemento de object1 no se compare con los elementos que ya se encuentran en el object2)

NiksVij
fuente
¿Puedes arreglar la sangría de tu código?
Colidyre
@colidyre ¿la sangría está bien ahora?
NiksVij
No, todavía hay problemas allí. Después del cabezal de función, el bloque también debe sangrarse.
Colidyre
Si. Lo reedité una vez más. Lo copié, lo pegué en el IDE y ahora está funcionando.
NiksVij
1

Puedes escribir tu propia función igual:

  • los dictados son iguales si: 1) todas las claves son iguales, 2) todos los valores son iguales
  • las listas son iguales si: todos los elementos son iguales y están en el mismo orden
  • las primitivas son iguales si a == b

Debido a que usted está tratando con JSON, tendrá tipos estándar de Python: dict, list, etc, por lo que puede hacer la comprobación de tipos duro if type(obj) == 'dict':, etc.

Ejemplo aproximado (no probado):

def json_equals(jsonA, jsonB):
    if type(jsonA) != type(jsonB):
        # not equal
        return False
    if type(jsonA) == dict:
        if len(jsonA) != len(jsonB):
            return False
        for keyA in jsonA:
            if keyA not in jsonB or not json_equal(jsonA[keyA], jsonB[keyA]):
                return False
    elif type(jsonA) == list:
        if len(jsonA) != len(jsonB):
            return False
        for itemA, itemB in zip(jsonA, jsonB):
            if not json_equal(itemA, itemB):
                return False
    else:
        return jsonA == jsonB
Gordon Bean
fuente
0

Para aquellos que deseen depurar los dos objetos JSON (generalmente, hay una referencia y un objetivo ), aquí hay una solución que puede usar. Enumerará la " ruta " de los diferentes / no coincidentes desde el objetivo hasta la referencia.

level La opción se utiliza para seleccionar la profundidad a la que le gustaría mirar.

show_variables La opción se puede activar para mostrar la variable relevante.

def compareJson(example_json, target_json, level=-1, show_variables=False):
  _different_variables = _parseJSON(example_json, target_json, level=level, show_variables=show_variables)
  return len(_different_variables) == 0, _different_variables

def _parseJSON(reference, target, path=[], level=-1, show_variables=False):  
  if level > 0 and len(path) == level:
    return []
  
  _different_variables = list()
  # the case that the inputs is a dict (i.e. json dict)  
  if isinstance(reference, dict):
    for _key in reference:      
      _path = path+[_key]
      try:
        _different_variables += _parseJSON(reference[_key], target[_key], _path, level, show_variables)
      except KeyError:
        _record = ''.join(['[%s]'%str(p) for p in _path])
        if show_variables:
          _record += ': %s <--> MISSING!!'%str(reference[_key])
        _different_variables.append(_record)
  # the case that the inputs is a list/tuple
  elif isinstance(reference, list) or isinstance(reference, tuple):
    for index, v in enumerate(reference):
      _path = path+[index]
      try:
        _target_v = target[index]
        _different_variables += _parseJSON(v, _target_v, _path, level, show_variables)
      except IndexError:
        _record = ''.join(['[%s]'%str(p) for p in _path])
        if show_variables:
          _record += ': %s <--> MISSING!!'%str(v)
        _different_variables.append(_record)
  # the actual comparison about the value, if they are not the same, record it
  elif reference != target:
    _record = ''.join(['[%s]'%str(p) for p in path])
    if show_variables:
      _record += ': %s <--> %s'%(str(reference), str(target))
    _different_variables.append(_record)

  return _different_variables
Chieh-I Chen
fuente