Сравнивать данные в JSON — ответы API, файлы настроек, файлы переводов — приходится часто. Но если сравнить два JSON как обычный текст, нередко получается множество различий при одинаковых данных. В этой статье объясняется, почему так происходит и как это исправить.
Откуда берутся ложные различия
Стандарт JSON (RFC 8259) определяет объект как «неупорядоченный набор пар имя/значение». То есть {"a":1,"b":2} и {"b":2,"a":1} означают одни и те же данные. Но в тексте порядок строк разный, и diff считает это различием. Кроме того, независимо от данных возникают такие различия:
- ширина отступа (2 пробела, 4 пробела, табуляция) и JSON, сжатый в одну строку;
- наличие пробела после двоеточия (
"a":1и"a": 1); - запись чисел (
1.0и1,1e2и100); - экранирование в строках (
"\u00e9"и"é").
Решение: разобрать и записать в одинаковом виде
Самый надёжный способ — разобрать оба JSON в данные и снова сериализовать их по одним и тем же правилам.
- Разберите оба текста как JSON. Если есть синтаксические ошибки, сначала исправьте их.
- Рекурсивно отсортируйте ключи всех объектов.
- Запишите результат с одинаковым отступом (например, 2 пробела).
- Сравните полученный текст построчно.
Например, в двух JSON ниже различается порядок ключей и порядок элементов массива.
{"name":"kim","age":30,"tags":["a","b"]}
{"age":30,"name":"kim","tags":["b","a"]}
После сортировки и перезаписи различие в порядке ключей исчезает, и остаётся только настоящее различие — порядок в массиве.
@@ -2,7 +2,7 @@ "age": 30, "name": "kim", "tags": [- "a",- "b"+ "b",+ "a" ] }
Массивы не сортируются
В отличие от объектов, у массивов порядок имеет значение. ["a","b"] и ["b","a"] — разные данные. Поэтому при нормализации массивы, как правило, не сортируют. Однако если порядок в массиве для задачи не важен, как у списка тегов, результат будет проще читать, если отсортировать его вручную перед сравнением. Что правильно, зависит от смысла данных.
Что меняет разбор
У способа «разобрать и записать заново» есть побочные эффекты, о которых стоит знать.
- Запись чисел унифицируется.
1.0и1превращаются в1и выглядят одинаково. Чаще всего это хорошо, но если важна сама запись, сравните и исходный текст. - Точность больших целых чисел. JavaScript хранит числа как 64-битные числа с плавающей точкой, поэтому целые больше
Number.MAX_SAFE_INTEGER(2^53 − 1 = 9007199254740991) точно не представляются. Например,9007199254740993после разбора становится9007199254740992. Будьте внимательны с JSON, где длинные идентификаторы хранятся числами. - Повторяющиеся ключи. RFC 8259 лишь рекомендует (SHOULD), чтобы имена внутри объекта были уникальны, а обработка повторов зависит от реализации.
JSON.parseв JavaScript оставляет последнее значение. - Комментарии и завершающие запятые.
// commentи завершающие запятые вроде[1, 2,]не входят в стандарт JSON (их допускают расширения вроде JSON5 и JSONC). Стандартный парсер выдаст ошибку.
Как это сделать в командной строке
Если установлен jq, опция -S (--sort-keys) выводит JSON с отсортированными ключами.
jq -S . before.json > a.json
jq -S . after.json > b.json
diff -u a.json b.json
Отличие от JSON Patch
Результат diff — построчный список изменений для чтения человеком. JSON Patch (RFC 6902) — стандарт, описывающий изменения путями в структуре JSON, например {"op":"replace","path":"/age","value":31}, а JSON Merge Patch (RFC 7396) — способ наложить на оригинал JSON, содержащий только изменённые части. Для обмена изменениями между программами подходят эти стандарты, а для проверки человеком — текстовый diff после сортировки.
Попробуйте в этом инструменте
Если включить в сравнении текстов опцию Сортировать ключи JSON, обе стороны будут разобраны, ключи отсортированы, текст переписан с отступом в 2 пробела, и только потом выполнится сравнение. Порядок элементов массивов не меняется, а при ошибке разбора инструмент сообщит, на какой стороне и на каком символе она произошла. Можно перетащить сразу два файла .json. Как читать результат в виде patch, описано в статье Как читать unified diff.