Как сравнить два объекта JSON с одинаковыми элементами в другом порядке равными?

Как я могу проверить, равны ли два объекта JSON в python, не считая порядка списков?

Например...

Документ JSON a:

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

Документ JSON b:

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

a и b должны сравниваться равными, хотя порядок списков "errors" отличается.

Ответ 1

Если вы хотите, чтобы два объекта с одинаковыми элементами, но в другом порядке для сравнения равны, то очевидная вещь - сравнить отсортированные копии их - например, для словарей, представленных вашими строками JSON a и 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

... но это не сработает, потому что в каждом случае элемент "errors" диктатора верхнего уровня представляет собой список с теми же элементами в другом порядке, а sorted() не пытается сортировать что угодно, кроме "верхнего" уровня итерации.

Чтобы исправить это, мы можем определить функцию ordered, которая будет рекурсивно сортировать любые найденные им списки (и конвертировать словари в списки пар (key, value), чтобы они были упорядочены):

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

Если мы применяем эту функцию к a и b, результаты сравниваются равными:

>>> ordered(a) == ordered(b)
True

Ответ 2

Другим способом может быть использование опции json.dumps(X, sort_keys=True):

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

Это работает для вложенных словарей и списков.

Ответ 3

Декодируйте их и сравните их как комментарий в mgilson.

Заказ не имеет значения для словаря, если ключи и значения совпадают. (Словарь не имеет порядка в Python)

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

Но порядок важен в списке; сортировка решит проблему для списков.

>>> [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

Следующий пример будет работать для JSON в вопросе. Для общего решения см. Ответ Zero Piraeus.

Ответ 4

Вы можете написать свою собственную функцию равенства:

  • Дикты равны, если: 1) все ключи равны, 2) все значения равны
  • списки равны, если: все элементы равны и в том же порядке
  • примитивы равны, если a == b

Поскольку вы имеете дело с json, у вас будут стандартные типы Python: dict, list и т.д., Поэтому вы можете выполнить жесткую проверку типов, if type(obj) == 'dict': и т.д.

Грубый пример (не проверен):

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

Ответ 5

Для следующих двух диктов 'dictWithListsInValue' и 'reorderedDictWithReorderedListsInValue', которые являются просто переупорядоченными версиями друг друга

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

дал мне неправильный результат, т.е. ложный.

Таким образом, я создал свой собственный ObjectComparator, как это:

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

который дал мне правильный ожидаемый результат!

Логика довольно проста:

Если объекты имеют тип "список", то сравнивают каждый элемент первого списка с элементами второго списка до тех пор, пока он не будет найден, а если элемент не найден после прохождения второго списка, то "найденный" будет = ложным. 'найденное' значение возвращается

Иначе, если сравниваемые объекты имеют тип 'dict', сравните значения, присутствующие для всех соответствующих ключей в обоих объектах. (Выполняется рекурсивное сравнение)

Еще просто позвоните obj1 == obj2. По умолчанию он работает нормально для объекта строк и чисел, и для них eq() определяется соответствующим образом.

(Обратите внимание, что алгоритм может быть дополнительно улучшен путем удаления элементов, найденных в object2, чтобы следующий элемент object1 не сравнивался с элементами, уже найденными в object2)