Сравнение JSON

Сравните два документа JSON и увидьте, что добавлено, удалено и изменено. Порядок ключей, отступы и минификация не в счёт, и ничего не покидает браузер.

Первый JSON
Второй JSON
Результат

Вставьте JSON в обе панели, чтобы сравнить их

Сравнение данных, а не текста

Построчное сравнение отвечает на вопрос о записи: какие строки этого файла не являются строками того. Для прозы и для исходного кода это верный вопрос, а для JSON — неверный, потому что у документа JSON нет единственно правильной записи. Переставьте члены объекта, измените отступы или сожмите одну из сторон — и изменится каждая строка, тогда как данные не сдвинутся ни на шаг. Этот инструмент сравнивает то, во что два документа разбираются, поэтому он сообщает о том, что действительно изменилось. Ниже один и тот же документ записан дважды: в первой строке сжато, затем с отступами и с членами в другом порядке.

{"name":"checkout","port":8080,"tags":["a","b"]}

{
  "port": 8080,
  "tags": ["a", "b"],
  "name": "checkout"
}

У этих двух нет ни одной общей строки, и этот инструмент не сообщает между ними никакого различия: те же члены, те же значения, тот же порядок внутри списка. Практическое следствие в том, что вам никогда не нужно переформатировать документ, чтобы сравнение заработало, — а это означало бы изменить как раз то, что вы собрались сравнивать.

Что никогда не считается различием

Четыре свойства записи документа игнорируются, и игнорируются безусловно. Нет никакого переключателя, который вернул бы любое из них обратно, потому что инструмент, способный на это, был бы двумя инструментами с двумя значениями, которые пришлось бы объяснять, а читателю пришлось бы знать, который из двух дал ответ у него перед глазами.

  • Порядок, в котором записаны члены объекта. {"a": 1, "b": 2} и {"b": 2, "a": 1} несут одни и те же данные, поэтому сериализатор, который на каждом запуске выдаёт ключи в другом порядке, никогда не появляется здесь как изменение.
  • Пробельные символы любого рода — отступы, переводы строк, пробел после двоеточия. Сжатый документ и документ с отступами, несущие одни и те же данные, — это один документ дважды.
  • Как записано число. 1e3 — это 1000, а 1.50 — это 1.5, потому что у числа JSON есть значение и нет написания: два документа, различающиеся только этим, несут одни и те же числа, и о них не сообщается ровно ничего.
  • Как экранирован символ внутри строки. Шесть символов \u0041 и буква A — это одна и та же строка из одного символа.

Две вещи, которым как будто место в этом списке, намеренно в него не входят. Порядок элементов списка — это данные ([1, 2] и [2, 1] суть разные документы), а член со значением null — не то же самое, что член, которого нет вовсе, поэтому одно против другого сообщается, а не проходит молча.

Добавлено, удалено, изменено — и смена типа

Каждая строка результата относится ровно к одному из трёх видов, и вместе они покрывают всё, что сравнение способно найти. Возьмите эти два документа:

{"host":"api.example.com","port":8080,"debug":true}

{"host":"api.example.com","port":"8080","retries":3}
  • Изменено в $['port']: член есть в обоих документах, и его значение не совпадает, поэтому показаны оба значения. Эта же строка помечена как смена типа, потому что 8080 — число, а "8080" — строка.
  • Удалено в $['debug']: член есть в первом документе и отсутствует во втором.
  • Добавлено в $['retries']: член есть во втором документе и отсутствует в первом.

Член по имени host есть в обоих с тем же значением, поэтому о нём не сообщается вовсе. Смена типа — это свойство изменения, а не четвёртый вид строки: именно её замечает схема и обычно не замечает человек, читающий два файла, поэтому о ней говорится там, где она случилась, вместо того чтобы оставлять её вам на глаз.

Где находится различие, записанное как путь

Каждая строка называет место, в котором находится, и называет его путём внутрь документа, а не номером строки: номер строки был бы фактом о записи — ровно о том, что этот инструмент только что закончил игнорировать. Пути — это нормализованные пути RFC 9535, единственная запись, которую этот сайт использует всюду, где положение внутри JSON нужно зафиксировать.

  • $ — это весь документ, $['port'] — член по имени port в нём, а $['hosts'][1] — элемент с индексом 1 в списке под hosts. Член всегда записывается в квадратных скобках и кавычках, поэтому имени с точкой, пробелом или собственной скобкой внутри ничего особенного не требуется.
  • Путь — это запрос, который можно выполнить. Вставьте его в Тестер JSONPath на этом сайте вместе с документом, которому он принадлежит, и он выберет ровно этот узел — самый быстрый способ прочитать различие в его окружении.
  • Это запрос к тому документу, в котором узел действительно находится: путь добавленного члена выполняется по второму документу, а удалённого — по первому. Ничто здесь никогда не называет узел, которого нет.

Изменённая строка может нести два пути, и в этом проступает сопоставление из следующего раздела: как только список приобрёл или потерял элемент, тот же самый элемент оказывается на разных индексах с каждой стороны. Там, где оба пути совпадают, — а это почти всегда — показывается только один.

Как сопоставляются два списка

Два объекта сравниваются член за членом, и это просто, потому что у члена есть имя. У двух списков имён нет, есть только позиции, и сравнение их позиция за позицией — как раз то, что делает большую часть вывода сравнений нечитаемой: вставьте одну запись в начало длинного списка, и уже ничто не там, где было, так что сравнение, читающее позицию как тождество, сообщает о различии всего списка — истина в узком смысле и бесполезность во всех остальных. Поэтому элементы сначала сопоставляются — по тому, что они собой представляют, а не по полю, которое вы назовёте, — и сообщается только то, что сопоставление оставило.

{"hosts": ["alpha", "beta", "gamma"]}

{"hosts": ["alpha", "staging", "beta", "gamma"]}

Это одна строка: staging добавлен в $['hosts'][1]. Элементы beta и gamma сдвинулись на одну позицию и не упоминаются, потому что сдвинуться — не значит измениться. То же сопоставление позволяет сообщить об одном отредактированном поле внутри элемента списка как об изменении именно в этом поле, а не как о замене всего элемента другим.

Порядок по-прежнему в счёт, и сопоставление не делает вид, что это не так: [1, 2] против [2, 1] возвращается как 1, удалённая из начала, и 1, добавленная после 2, — что и произошло. Два списка могут оказаться и слишком разными, чтобы сопоставление окупало себя, и за этой чертой сравнение переходит на проход позиция за позицией и говорит об этом наверху результата — так шумный ответ оказывается объяснённым, а не загадочным.

Отчёт и дерево

Одно и то же сравнение предлагается в двух видах, и переключение между ними ничего не пересчитывает: дерево рисуется по тому самому списку различий, который перечисляет отчёт, поэтому они не могут разойтись ни в том, что изменилось, ни в том, сколько этого.

  • Отчёт отвечает на вопрос, что изменилось: по строке на различие, каждая со своим путём, своим видом и своим значением с каждой стороны. Это вид, который просматривают, когда хочется видеть весь список сразу.
  • Дерево отвечает на вопрос, где в документе: два документа, слитые в один план, и раскрыты только те части, которые несут различие. Каждый отрезок подряд идущих потомков, не несущих ничего, сворачивается в одну строку, говорящую, за сколько она стоит, — пара из раздела выше рисуется пятью строками: документ, список внутри него, свёрнутая строка за один элемент, добавленный элемент и свёрнутая строка за два.
  • Счётчик рядом с заголовком результата — это все различия, какие есть. Когда список на экране обрезан по длине, этот счётчик не двигается, а кнопка внизу списка показывает остальное.
~! $['port'] 8080 -> "8080"
- $['debug'] true
+ $['retries'] 3

Вот что выдаёт кнопка копирования для двух документов из третьего раздела: знак вида, затем путь, затем значения со стрелкой между ними — плюс для добавленного члена, минус для удалённого, тильда для изменения и тильда с восклицательным знаком там, где сменился ещё и тип. Это записано символами, а не словами намеренно, чтобы вставленный результат был одним и тем же текстом, на каком бы из языков сайта вы ни читали, и он ничего не укорачивает: значение, обрезанное экраном, лежит там целиком.

Чего стоит разбор и о чём инструмент говорит вслух

Сравнивать то, во что разбираются два документа, значит мириться с тем, чего стоит разбор, а разбор JSON не проходит без потерь. Здесь это весит больше, чем где-либо ещё на этом сайте, потому что ответ, который инструмент сравнения меньше всего может себе позволить, — «различий нет» между двумя документами, которые на деле различаются. Поэтому текст каждой панели читается второй раз как текст, и то, что разбор обронил, сообщается рядом со сравнением, а не проглатывается.

  • Имя члена, записанное дважды в одном объекте. Разбор переживает только последняя копия, поэтому сравнивалась только она, а предыдущую не видел никто.
  • Число, слишком длинное, чтобы уцелеть в виде двойной точности. Два разных литерала могут разобраться в одно значение, и идентификаторы — как раз то место, где это случается: 12345678901234567890 и 12345678901234567891 суть разные целые и одно разобранное число, так что сравнение само по себе назвало бы одинаковыми два документа, у которых идентификаторы различаются.
{"user": {"id": 1}, "user": {"id": 2}}

Этот документ разбирается в {"user": {"id": 2}}, и инструмент говорит об этом под панелью, в которую он был вставлен, со строкой и столбцом первого случая и с их общим числом. Ни одна из двух потерь не останавливает сравнение; обе стоят рядом с ним. Документ, который не разбирается вовсе, сообщается там же, с положением проблемы всюду, где его есть чем назвать, и обе панели читаются независимо — так что два сломанных документа дают две ошибки сразу, а не одну за другой. Документ, вложенный куда глубже, чем значение можно нарисовать, отклоняется сразу, и это отказ, а не зависшая вкладка.

Почему ничто из вставленного не покидает эту вкладку

Оба документа остаются в вашем браузере. Нет ни загрузки, ни запроса, ни сервера, который мог бы держать копию: сравнение — это арифметика, выполняющаяся внутри уже загруженной вами страницы, и потому же оно продолжает работать при выключенной сети. Именно это делает безопасной вставку сюда ответа из продакшена, карточки клиента или подписанной полезной нагрузки.

Ничего и не сохраняется. Перезагрузите страницу — и обе панели пусты; нет ни истории, ни учётной записи, ни чего-либо, что нужно потом удалять. Единственное, что когда-либо отсюда уходит, — это то, что вы скопируете сами.

Частые вопросы

Порядок ключей действительно игнорируется?
Да, всегда, и ради этого инструмент и существует. Два объекта с одними и теми же членами и значениями одинаковы, как бы они ни были записаны, поэтому сериализатор, который на каждом запуске выдаёт ключи в другом порядке, никогда не появляется как изменение. Отступы, сжатие, запись числа и экранирование символа игнорируются по той же причине, и ничто из этого не настройка, которую вы могли бы переключить иначе.
Член со значением null — это то же самое, что отсутствующий член?
Нет, и нет опции считать их одним и тем же. {"note": null} против {} сообщается как $['note'] удалён. Присутствующее пустое поле — иное утверждение, чем поле, которого никто не прислал: любой язык схем и любой язык со статической типизацией различает их, — так что слить их здесь значило бы выбросить то, что документы на самом деле говорят.
[1, 2] и [2, 1] сообщаются как одинаковые?
Нет. Массив JSON упорядочен, поэтому два списка с одними и теми же элементами в разном порядке — два разных документа, и назвать их равными значило бы заставить этот инструмент сказать о формате неправду. Вы получаете удаление и добавление: 1 удалена из начала и добавлена снова после 2. С объектами наоборот, потому что их члены названы, а не расставлены.
Что означает путь в каждой строке?
Это место, где различие сидит внутри документа, записанное как нормализованный путь RFC 9535: $ — сам документ, $['port'] — его член, $['hosts'][1] — элемент с индексом 1 в списке под hosts. Это ещё и запрос, который можно выполнить: вставьте его в Тестер JSONPath на этом сайте вместе с документом, которому он принадлежит, и он выберет ровно этот узел, и никакой другой.
Почему в одной строке показались два разных пути?
Потому что сопоставление двух списков может оставить один и тот же элемент на разных индексах с каждой стороны. Сравните ["alpha", "beta"] с ["intro", "alpha", "beta!"] — и получите две строки: intro добавлен в $[0], а beta стал beta!, что есть $[1] в первом документе и $[2] во втором. Там, где оба пути совпадают, а обычно они совпадают, показывается только один.
Что означает смена типа в строке?
Что значение не просто другое, а другого рода: число, ставшее строкой; значение, ставшее null; объект, ставший списком. Порт, записанный 8080 с одной стороны и "8080" с другой, — самый частый случай, и обычно он означает, что сменился сериализатор или клиент, а не данные. Это помечается в строке, потому что отредактированное значение и уплывающий контракт требуют от вас разного.
Мои два документа выглядят одинаково, но один пишет один и тот же ключ дважды. Что происходит?
Разбор оставляет последнюю копию и роняет предыдущую, поэтому сравнение видит один член и вполне может не сообщить никакого различия. Именно ради этого инструмент читает сырой текст отдельно и говорит под той панелью, что имя записано дважды, сколько раз всего и где находится первое. То же второе чтение ловит целое, слишком длинное, чтобы уцелеть в виде двойной точности, — другой способ, которым два различающихся документа могут разобраться в два неразличимых.
Можно получить патч, который я смогу применить?
Нет, и это решение, а не нехватка. JSON Patch применяется операция за операцией, поэтому индексы внутри него сдвигаются по ходу выполнения; выдать такой патч — значит дать обещание о программе, которую вы запустите против собственных данных, а это утверждение куда более сильное, чем описание различий. Забрать с собой вместо этого можно отчёт текстом, из кнопки копирования: одинаковый на всех языках сайта и нигде не укороченный.
Отправляется ли что-нибудь из вставленного на сервер?
Нет. Оба документа сравниваются в вашем браузере, и ни один его не покидает: ни загрузки, ни запроса, ни чего-либо, сохраняемого между посещениями. Именно это делает безопасной вставку сюда ответа из продакшена, и потому же инструмент работает при выключенной сети. Перезагрузите страницу — и обе панели снова пусты.

Похожие инструменты

  • Сравнение текстов

    Когда изменение, которое вы хотите увидеть, — в записи, а не в данных: прогон отступов, переехавший ключ или два файла, которые вовсе не JSON, — его показывает построчное сравнение. Именно это делает та страница, с настройками пробелов и регистра и копируемым патчем.

  • Форматировщик JSON

    Проверяет и красиво форматирует JSON, с точными местами ошибок.

  • Форматировщик SQL

    Форматирует и украшает SQL — несколько диалектов.

  • Форматировщик XML

    Форматирует и проверяет XML — украсить или сжать.