From 401af52511f1e16a2604e9f58b63e3e7293292d7 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 28 Sep 2026 23:13:00 +0200 Subject: [PATCH] Add comparisons to json_view basic_json_view gains operator== and operator!= with other views and with basic_json values. Two views are equal if the values parse() would produce for them are equal by basic_json's operator==: numbers compare by value across their types, and objects by their members, with duplicate keys resolved as parse() resolves them (the last value, at the position of the first key). Objects are compared in member order if the object type keeps an order (ordered_json), by key otherwise, as basic_json does. Discarded views compare as discarded basic_json values do, which follows JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON. Nothing is materialized except single numbers, and the walk is iterative. Tests compare the results for pairs of 1,200 generated documents (also written differently: sorted keys, canonical numbers) with those of basic_json, for json and ordered_json, plus numbers, duplicate keys, member order, discarded values, and 100,000 levels of nesting. Signed-off-by: Niels Lohmann --- BUILD.bazel | 1 + include/nlohmann/detail/view/compare.hpp | 320 ++++++++++++++++++++ include/nlohmann/json_view.hpp | 46 +++ single_include/nlohmann/json_view.hpp | 368 +++++++++++++++++++++++ tests/src/unit-json_view.cpp | 84 ++++++ 5 files changed, 819 insertions(+) create mode 100644 include/nlohmann/detail/view/compare.hpp diff --git a/BUILD.bazel b/BUILD.bazel index 20813cff1..92f0149c5 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -67,6 +67,7 @@ cc_library( "include/nlohmann/detail/string_utils.hpp", "include/nlohmann/detail/value_t.hpp", "include/nlohmann/detail/view/builder.hpp", + "include/nlohmann/detail/view/compare.hpp", "include/nlohmann/detail/view/document_data.hpp", "include/nlohmann/detail/view/errors.hpp", "include/nlohmann/detail/view/input.hpp", diff --git a/include/nlohmann/detail/view/compare.hpp b/include/nlohmann/detail/view/compare.hpp new file mode 100644 index 000000000..551c4013e --- /dev/null +++ b/include/nlohmann/detail/view/compare.hpp @@ -0,0 +1,320 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // sort, stable_sort +#include // size_t +#include // move, pair +#include // vector + +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Equality of views, and of views with basic_json values, with the semantics +// of basic_json's operator== applied to the values parse() would produce: +// numbers compare by value across their types, an object is compared by its +// members with duplicate keys resolved as parse() resolves them (the last +// value, at the position of the first occurrence), and in document order if +// the object type keeps an order (ordered_json), by key otherwise. + +/// one side of a comparison: a view +template +class view_side +{ + public: + using string_view_t = typename View::string_view_t; + + explicit view_side(const View& v) noexcept + : m_view(v) + {} + + value_t type() const noexcept + { + return m_view.type(); + } + + std::size_t size() const noexcept + { + return m_view.size(); + } + + string_view_t string() const + { + return m_view.get_string(); + } + + /// a number, boolean, or null as a basic_json value (no allocation) + BasicJsonType scalar() const + { + switch (m_view.type()) + { + case value_t::number_integer: + return BasicJsonType(m_view.template get()); + case value_t::number_unsigned: + return BasicJsonType(m_view.template get()); + case value_t::number_float: + return BasicJsonType(m_view.template get()); + case value_t::boolean: + return BasicJsonType(m_view.template get()); + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + return BasicJsonType(nullptr); + } + } + + void elements(std::vector& out) const + { + out.reserve(m_view.size()); + for (const View e : m_view) + { + out.emplace_back(e); + } + } + + /// the members as parse() keeps them: one per key, the last value at the + /// position of the first occurrence; in that order, or sorted by key + void members(std::vector>& out, bool ordered) const + { + struct member + { + string_view_t key; + View value; + std::size_t position; + }; + std::vector all; + all.reserve(m_view.size()); + std::size_t position = 0; + for (auto it = m_view.begin(); it != m_view.end(); ++it) + { + all.push_back(member{it.key(), it.value(), position++}); + } + std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b) + { + return a.key < b.key; + }); + std::vector unique; + unique.reserve(all.size()); + for (std::size_t i = 0; i < all.size();) + { + std::size_t last = i; + while (last + 1 < all.size() && all[last + 1].key == all[i].key) + { + ++last; + } + unique.push_back(member{all[i].key, all[last].value, all[i].position}); + i = last + 1; + } + if (ordered) + { + std::sort(unique.begin(), unique.end(), [](const member & a, const member & b) + { + return a.position < b.position; + }); + } + out.reserve(unique.size()); + for (const member& m : unique) + { + out.emplace_back(m.key, view_side(m.value)); + } + } + + private: + View m_view; +}; + +/// the other side of a comparison: a basic_json value +template +class json_side +{ + public: + using string_view_t = StringView; + + explicit json_side(const BasicJsonType& j) noexcept + : m_json(&j) + {} + + value_t type() const noexcept + { + return m_json->type(); + } + + std::size_t size() const noexcept + { + return m_json->size(); + } + + string_view_t string() const + { + const auto& s = m_json->template get_ref(); + return string_view_t(s.data(), s.size()); + } + + BasicJsonType scalar() const + { + return *m_json; + } + + void elements(std::vector& out) const + { + out.reserve(m_json->size()); + for (const auto& e : *m_json) + { + out.emplace_back(e); + } + } + + void members(std::vector>& out, bool ordered) const + { + out.reserve(m_json->size()); + for (auto it = m_json->cbegin(); it != m_json->cend(); ++it) + { + out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value())); + } + if (!ordered) + { + std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + } + } + + private: + const BasicJsonType* m_json; +}; + +/// whether two sides are equal; iterative, so that the nesting depth is +/// limited by memory only +template +bool equal(const A& a0, const B& b0) +{ + using string_view_t = typename A::string_view_t; + const bool ordered = is_ordered_map::value; + + struct frame + { + std::vector elements_a{}; + std::vector elements_b{}; + std::vector> members_a{}; + std::vector> members_b{}; + bool object = false; + std::size_t next = 0; + }; + std::vector stack; + A a = a0; + B b = b0; + for (;;) + { + const value_t ta = a.type(); + const value_t tb = b.type(); + const bool numbers = (ta == value_t::number_integer || ta == value_t::number_unsigned || ta == value_t::number_float) + && (tb == value_t::number_integer || tb == value_t::number_unsigned || tb == value_t::number_float); + if (ta == value_t::discarded || tb == value_t::discarded) + { + // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON) + if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded))) + { + return false; + } + } + else if (numbers) + { + if (!(a.scalar() == b.scalar())) + { + return false; + } + } + else if (ta != tb) + { + return false; + } + else if (ta == value_t::string) + { + if (!(a.string() == b.string())) + { + return false; + } + } + else if (ta == value_t::array || ta == value_t::object) + { + if (a.size() != b.size() && ta == value_t::array) + { + return false; + } + frame f; + f.object = ta == value_t::object; + if (f.object) + { + a.members(f.members_a, ordered); + b.members(f.members_b, ordered); + if (f.members_a.size() != f.members_b.size()) + { + return false; + } + } + else + { + a.elements(f.elements_a); + b.elements(f.elements_b); + } + stack.push_back(std::move(f)); + } + else if (!(a.scalar() == b.scalar())) // null, boolean + { + return false; + } + + // the next pair of values + for (;;) + { + if (stack.empty()) + { + return true; + } + frame& f = stack.back(); + const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); + if (f.next == count) + { + stack.pop_back(); + continue; + } + if (f.object) + { + if (!(f.members_a[f.next].first == f.members_b[f.next].first)) + { + return false; + } + a = f.members_a[f.next].second; + b = f.members_b[f.next].second; + } + else + { + a = f.elements_a[f.next]; + b = f.elements_b[f.next]; + } + ++f.next; + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 7ed800b7a..9b05e4148 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -47,6 +47,7 @@ #endif #include +#include #include #include #include @@ -603,6 +604,44 @@ class basic_json_view } #endif + //////////////// + // comparison // + //////////////// + + /// whether the values parse() would produce for two views are equal, as + /// by BasicJsonType's operator== (numbers by value, objects by their + /// members with duplicate keys resolved as parse() resolves them) + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), side(b)); + } + + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// whether the value parse() would produce for a view equals a value + friend bool operator==(const basic_json_view& a, const BasicJsonType& j) + { + return detail::view::equal(side(a), json_side_t(j)); + } + + friend bool operator==(const BasicJsonType& j, const basic_json_view& a) + { + return a == j; + } + + friend bool operator!=(const basic_json_view& a, const BasicJsonType& j) + { + return !(a == j); + } + + friend bool operator!=(const BasicJsonType& j, const basic_json_view& a) + { + return !(a == j); + } + ///////////////// // materialize // ///////////////// @@ -635,6 +674,13 @@ class basic_json_view : m_doc(d), m_node(n) {} + using json_side_t = detail::view::json_side; + + static detail::view::view_side side(const basic_json_view& v) noexcept + { + return detail::view::view_side(v); + } + /// the number of source bytes of this value (estimated for values with /// decoded strings) std::size_t source_extent() const noexcept diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index bc0ca03b9..6d340c198 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -1567,6 +1567,329 @@ inline bool build(document_data& d, const char* src, std::size_t size, bool comm } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // sort, stable_sort +#include // size_t +#include // move, pair +#include // vector + +// #include +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Equality of views, and of views with basic_json values, with the semantics +// of basic_json's operator== applied to the values parse() would produce: +// numbers compare by value across their types, an object is compared by its +// members with duplicate keys resolved as parse() resolves them (the last +// value, at the position of the first occurrence), and in document order if +// the object type keeps an order (ordered_json), by key otherwise. + +/// one side of a comparison: a view +template +class view_side +{ + public: + using string_view_t = typename View::string_view_t; + + explicit view_side(const View& v) noexcept + : m_view(v) + {} + + value_t type() const noexcept + { + return m_view.type(); + } + + std::size_t size() const noexcept + { + return m_view.size(); + } + + string_view_t string() const + { + return m_view.get_string(); + } + + /// a number, boolean, or null as a basic_json value (no allocation) + BasicJsonType scalar() const + { + switch (m_view.type()) + { + case value_t::number_integer: + return BasicJsonType(m_view.template get()); + case value_t::number_unsigned: + return BasicJsonType(m_view.template get()); + case value_t::number_float: + return BasicJsonType(m_view.template get()); + case value_t::boolean: + return BasicJsonType(m_view.template get()); + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + return BasicJsonType(nullptr); + } + } + + void elements(std::vector& out) const + { + out.reserve(m_view.size()); + for (const View e : m_view) + { + out.emplace_back(e); + } + } + + /// the members as parse() keeps them: one per key, the last value at the + /// position of the first occurrence; in that order, or sorted by key + void members(std::vector>& out, bool ordered) const + { + struct member + { + string_view_t key; + View value; + std::size_t position; + }; + std::vector all; + all.reserve(m_view.size()); + std::size_t position = 0; + for (auto it = m_view.begin(); it != m_view.end(); ++it) + { + all.push_back(member{it.key(), it.value(), position++}); + } + std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b) + { + return a.key < b.key; + }); + std::vector unique; + unique.reserve(all.size()); + for (std::size_t i = 0; i < all.size();) + { + std::size_t last = i; + while (last + 1 < all.size() && all[last + 1].key == all[i].key) + { + ++last; + } + unique.push_back(member{all[i].key, all[last].value, all[i].position}); + i = last + 1; + } + if (ordered) + { + std::sort(unique.begin(), unique.end(), [](const member & a, const member & b) + { + return a.position < b.position; + }); + } + out.reserve(unique.size()); + for (const member& m : unique) + { + out.emplace_back(m.key, view_side(m.value)); + } + } + + private: + View m_view; +}; + +/// the other side of a comparison: a basic_json value +template +class json_side +{ + public: + using string_view_t = StringView; + + explicit json_side(const BasicJsonType& j) noexcept + : m_json(&j) + {} + + value_t type() const noexcept + { + return m_json->type(); + } + + std::size_t size() const noexcept + { + return m_json->size(); + } + + string_view_t string() const + { + const auto& s = m_json->template get_ref(); + return string_view_t(s.data(), s.size()); + } + + BasicJsonType scalar() const + { + return *m_json; + } + + void elements(std::vector& out) const + { + out.reserve(m_json->size()); + for (const auto& e : *m_json) + { + out.emplace_back(e); + } + } + + void members(std::vector>& out, bool ordered) const + { + out.reserve(m_json->size()); + for (auto it = m_json->cbegin(); it != m_json->cend(); ++it) + { + out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value())); + } + if (!ordered) + { + std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + } + } + + private: + const BasicJsonType* m_json; +}; + +/// whether two sides are equal; iterative, so that the nesting depth is +/// limited by memory only +template +bool equal(const A& a0, const B& b0) +{ + using string_view_t = typename A::string_view_t; + const bool ordered = is_ordered_map::value; + + struct frame + { + std::vector elements_a{}; + std::vector elements_b{}; + std::vector> members_a{}; + std::vector> members_b{}; + bool object = false; + std::size_t next = 0; + }; + std::vector stack; + A a = a0; + B b = b0; + for (;;) + { + const value_t ta = a.type(); + const value_t tb = b.type(); + const bool numbers = (ta == value_t::number_integer || ta == value_t::number_unsigned || ta == value_t::number_float) + && (tb == value_t::number_integer || tb == value_t::number_unsigned || tb == value_t::number_float); + if (ta == value_t::discarded || tb == value_t::discarded) + { + // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON) + if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded))) + { + return false; + } + } + else if (numbers) + { + if (!(a.scalar() == b.scalar())) + { + return false; + } + } + else if (ta != tb) + { + return false; + } + else if (ta == value_t::string) + { + if (!(a.string() == b.string())) + { + return false; + } + } + else if (ta == value_t::array || ta == value_t::object) + { + if (a.size() != b.size() && ta == value_t::array) + { + return false; + } + frame f; + f.object = ta == value_t::object; + if (f.object) + { + a.members(f.members_a, ordered); + b.members(f.members_b, ordered); + if (f.members_a.size() != f.members_b.size()) + { + return false; + } + } + else + { + a.elements(f.elements_a); + b.elements(f.elements_b); + } + stack.push_back(std::move(f)); + } + else if (!(a.scalar() == b.scalar())) // null, boolean + { + return false; + } + + // the next pair of values + for (;;) + { + if (stack.empty()) + { + return true; + } + frame& f = stack.back(); + const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); + if (f.next == count) + { + stack.pop_back(); + continue; + } + if (f.object) + { + if (!(f.members_a[f.next].first == f.members_b[f.next].first)) + { + return false; + } + a = f.members_a[f.next].second; + b = f.members_b[f.next].second; + } + else + { + a = f.elements_a[f.next]; + b = f.elements_b[f.next]; + } + ++f.next; + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // #include @@ -3757,6 +4080,44 @@ class basic_json_view } #endif + //////////////// + // comparison // + //////////////// + + /// whether the values parse() would produce for two views are equal, as + /// by BasicJsonType's operator== (numbers by value, objects by their + /// members with duplicate keys resolved as parse() resolves them) + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), side(b)); + } + + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// whether the value parse() would produce for a view equals a value + friend bool operator==(const basic_json_view& a, const BasicJsonType& j) + { + return detail::view::equal(side(a), json_side_t(j)); + } + + friend bool operator==(const BasicJsonType& j, const basic_json_view& a) + { + return a == j; + } + + friend bool operator!=(const basic_json_view& a, const BasicJsonType& j) + { + return !(a == j); + } + + friend bool operator!=(const BasicJsonType& j, const basic_json_view& a) + { + return !(a == j); + } + ///////////////// // materialize // ///////////////// @@ -3789,6 +4150,13 @@ class basic_json_view : m_doc(d), m_node(n) {} + using json_side_t = detail::view::json_side; + + static detail::view::view_side side(const basic_json_view& v) noexcept + { + return detail::view::view_side(v); + } + /// the number of source bytes of this value (estimated for values with /// decoded strings) std::size_t source_extent() const noexcept diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index c6defd558..c999f358a 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -1130,3 +1130,87 @@ TEST_CASE("json_view dump") CHECK(json_view().dump() == json(json::value_t::discarded).dump()); } } + +TEST_CASE("json_view comparison") +{ + SECTION("equality of the values parse() produces") + { + generator g; + std::vector texts; + for (int i = 0; i < 600; ++i) + { + std::string text; + g.value(text, 0); + texts.push_back(text); + // the same value written differently: sorted keys, canonical numbers + texts.push_back(json::parse(text).dump(1)); + } + for (std::size_t i = 0; i + 2 < texts.size(); ++i) + { + for (std::size_t k = i; k < i + 3; ++k) + { + CAPTURE(texts[i]); + CAPTURE(texts[k]); + const json_document a = json_document::parse(texts[i]); + const json_document b = json_document::parse(texts[k]); + const json ja = json::parse(texts[i]); + const json jb = json::parse(texts[k]); + CHECK((a.root() == b.root()) == (ja == jb)); + CHECK((a.root() != b.root()) == (ja != jb)); + CHECK((a.root() == jb) == (ja == jb)); + CHECK((jb == a.root()) == (ja == jb)); + CHECK((a.root() != jb) == (ja != jb)); + CHECK((jb != a.root()) == (ja != jb)); + + // ordered_json compares members in order + const ordered_json_document oa = ordered_json_document::parse(texts[i]); + const ordered_json_document ob = ordered_json_document::parse(texts[k]); + const ordered_json oja = ordered_json::parse(texts[i]); + const ordered_json ojb = ordered_json::parse(texts[k]); + CHECK((oa.root() == ob.root()) == (oja == ojb)); + CHECK((oa.root() == ojb) == (oja == ojb)); + } + } + } + + SECTION("numbers, duplicate keys, member order") + { + const auto same = [](const char* x, const char* y) + { + return json_document::parse(x).root() == json_document::parse(y).root(); + }; + CHECK(same("1", "1.0")); + CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]")); + CHECK(!same("1", "1.5")); + CHECK(same("18446744073709551615", "18446744073709551615")); + CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})")); + CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})")); + CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})")); + CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})")); + CHECK(!same("[1, 2]", "[2, 1]")); + CHECK(!same("\"a\"", "\"b\"")); + CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\"")); + CHECK(!same("null", "false")); + CHECK(!same("[]", "{}")); + CHECK(ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})").root() == ordered_json_document::parse(R"({"a": 3, "b": 2})").root()); + CHECK(ordered_json_document::parse(R"({"a": 1, "b": 2})").root() != ordered_json_document::parse(R"({"b": 2, "a": 1})").root()); + + // discarded values compare as basic_json's do + const json discarded(json::value_t::discarded); + CHECK((json_view() == json_view()) == (discarded == discarded)); + CHECK((json_view() == discarded) == (discarded == discarded)); + CHECK(!(json_view() == json_document::parse("null").root())); + CHECK(!(json_document::parse("null").root() == discarded)); + } + + SECTION("deep nesting") + { + const std::string deep = std::string(100000, '[') + std::string(100000, ']'); + const json_document a = json_document::parse(deep); + const json_document b = json_document::parse(deep); + CHECK(a.root() == b.root()); + CHECK(a.root() == json::parse(deep)); + const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']'); + CHECK(a.root() != json_document::parse(other).root()); + } +}