| // __ _____ _____ _____ | |
| // __| | __| | | | JSON for Modern C++ (supporting code) | |
| // | | |__ | | | | | | version 3.12.0 | |
| // |_____|_____|_____|_|___| https://github.com/nlohmann/json | |
| // | |
| // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me> | |
| // SPDX-License-Identifier: MIT | |
| #include "doctest_compatibility.h" | |
| #include <nlohmann/json.hpp> | |
| using nlohmann::json; | |
| using nlohmann::ordered_json; | |
| #include <limits> | |
| #include <set> | |
| #include <string> | |
| #include <unordered_set> | |
| namespace | |
| { | |
| // how detail::hash defines the hash of an array or object: the seeds of the | |
| // elements, combined in order. Recursive, so only usable on values nested a | |
| // few hundred levels deep - which is exactly what is needed to check that the | |
| // iterative path taken below detail::recursion_depth_limit() computes the same. | |
| template<typename BasicJsonType> | |
| std::size_t reference_hash(const BasicJsonType& j) | |
| { | |
| using nlohmann::detail::combine; | |
| using string_t = typename BasicJsonType::string_t; | |
| if (!j.is_structured()) | |
| { | |
| return std::hash<BasicJsonType> {}(j); | |
| } | |
| auto seed = combine(static_cast<std::size_t>(j.type()), j.size()); | |
| for (const auto& element : j.items()) | |
| { | |
| if (j.is_object()) | |
| { | |
| seed = combine(seed, std::hash<string_t> {}(element.key())); | |
| } | |
| seed = combine(seed, reference_hash(element.value())); | |
| } | |
| return seed; | |
| } | |
| // a value nested `depth` levels deep, with siblings on every level | |
| template<typename BasicJsonType> | |
| BasicJsonType nested(const std::size_t depth, const bool objects) | |
| { | |
| BasicJsonType value = "leaf"; | |
| for (std::size_t i = 0; i < depth; ++i) | |
| { | |
| if (objects) | |
| { | |
| value = BasicJsonType{{"before", i}, {"nested", std::move(value)}, {"after", {i, "x"}}}; | |
| } | |
| else | |
| { | |
| value = BasicJsonType::array({i, std::move(value), BasicJsonType::object({{"k", i}})}); | |
| } | |
| } | |
| return value; | |
| } | |
| std::string nested_text(const std::size_t depth, const bool objects) | |
| { | |
| std::string text; | |
| if (objects) | |
| { | |
| text.reserve((6 * depth) + 1); | |
| for (std::size_t i = 0; i < depth; ++i) | |
| { | |
| text += "{\"a\":"; | |
| } | |
| text += "1"; | |
| text.append(depth, '}'); | |
| } | |
| else | |
| { | |
| text.assign(depth, '['); | |
| text += "1"; | |
| text.append(depth, ']'); | |
| } | |
| return text; | |
| } | |
| } // namespace | |
| TEST_CASE("hash<nlohmann::json>") | |
| { | |
| // Collect hashes for different JSON values and make sure that they are distinct | |
| // We cannot compare against fixed values, because the implementation of | |
| // std::hash may differ between compilers. | |
| // | |
| // numbers that compare equal under operator== (0 == 0U == 0.0) must hash | |
| // equally, so they are only inserted once below and checked separately. | |
| std::set<std::size_t> hashes; | |
| // null | |
| hashes.insert(std::hash<json> {}(json(nullptr))); | |
| // boolean | |
| hashes.insert(std::hash<json> {}(json(true))); | |
| hashes.insert(std::hash<json> {}(json(false))); | |
| // string | |
| hashes.insert(std::hash<json> {}(json(""))); | |
| hashes.insert(std::hash<json> {}(json("foo"))); | |
| // number | |
| hashes.insert(std::hash<json> {}(json(0))); | |
| hashes.insert(std::hash<json> {}(json(-1))); | |
| hashes.insert(std::hash<json> {}(json(42.23))); | |
| // array | |
| hashes.insert(std::hash<json> {}(json::array())); | |
| hashes.insert(std::hash<json> {}(json::array({1, 2, 3}))); | |
| // object | |
| hashes.insert(std::hash<json> {}(json::object())); | |
| hashes.insert(std::hash<json> {}(json::object({{"foo", "bar"}}))); | |
| // binary | |
| hashes.insert(std::hash<json> {}(json::binary({}))); | |
| hashes.insert(std::hash<json> {}(json::binary({}, 0))); | |
| hashes.insert(std::hash<json> {}(json::binary({}, 42))); | |
| hashes.insert(std::hash<json> {}(json::binary({1, 2, 3}))); | |
| hashes.insert(std::hash<json> {}(json::binary({1, 2, 3}, 0))); | |
| hashes.insert(std::hash<json> {}(json::binary({1, 2, 3}, 42))); | |
| // discarded | |
| hashes.insert(std::hash<json> {}(json(json::value_t::discarded))); | |
| CHECK(hashes.size() == 19); | |
| // numbers that compare equal under operator== must hash equally, | |
| // regardless of which of number_integer, number_unsigned, or | |
| // number_float actually holds the value | |
| CHECK(json(0) == json(static_cast<unsigned>(0))); | |
| CHECK(json(0) == json(0.0)); | |
| CHECK(std::hash<json> {}(json(0)) == std::hash<json> {}(json(static_cast<unsigned>(0)))); | |
| CHECK(std::hash<json> {}(json(0)) == std::hash<json> {}(json(0.0))); | |
| CHECK(std::hash<json> {}(json(-1)) == std::hash<json> {}(json(-1.0))); | |
| // a std::unordered_set relies on this same consistency between == and hash | |
| const std::unordered_set<json> numbers {json(0), json(static_cast<unsigned>(0)), json(0.0)}; | |
| CHECK(numbers.size() == 1); | |
| // -0.0 compares equal to 0 and 0.0 | |
| CHECK(json(-0.0) == json(0)); | |
| CHECK(std::hash<json> {}(json(-0.0)) == std::hash<json> {}(json(0))); | |
| CHECK(std::hash<json> {}(json(-0.0)) == std::hash<json> {}(json(0.0))); | |
| // the ends of the integer ranges, which equal floats exactly | |
| const auto int_min = (std::numeric_limits<json::number_integer_t>::min)(); | |
| const auto int_max = (std::numeric_limits<json::number_integer_t>::max)(); | |
| const auto two_63 = static_cast<json::number_unsigned_t>(1) << 63U; | |
| CHECK(json(int_min) == json(-9223372036854775808.0)); | |
| CHECK(std::hash<json> {}(json(int_min)) == std::hash<json> {}(json(-9223372036854775808.0))); | |
| CHECK(json(two_63) == json(9223372036854775808.0)); | |
| CHECK(std::hash<json> {}(json(two_63)) == std::hash<json> {}(json(9223372036854775808.0))); | |
| CHECK(json(static_cast<json::number_unsigned_t>(int_max)) == json(int_max)); | |
| CHECK(std::hash<json> {}(json(static_cast<json::number_unsigned_t>(int_max))) == std::hash<json> {}(json(int_max))); | |
| } | |
| TEST_CASE("hash<nlohmann::ordered_json>") | |
| { | |
| // Collect hashes for different JSON values and make sure that they are distinct | |
| // We cannot compare against fixed values, because the implementation of | |
| // std::hash may differ between compilers. | |
| std::set<std::size_t> hashes; | |
| // null | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json(nullptr))); | |
| // boolean | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json(true))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json(false))); | |
| // string | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json(""))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json("foo"))); | |
| // number | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json(0))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json(-1))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json(42.23))); | |
| // array | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::array())); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::array({1, 2, 3}))); | |
| // object | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::object())); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::object({{"foo", "bar"}}))); | |
| // binary | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::binary({}))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::binary({}, 0))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::binary({}, 42))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::binary({1, 2, 3}))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::binary({1, 2, 3}, 0))); | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json::binary({1, 2, 3}, 42))); | |
| // discarded | |
| hashes.insert(std::hash<ordered_json> {}(ordered_json(ordered_json::value_t::discarded))); | |
| CHECK(hashes.size() == 19); | |
| CHECK(std::hash<ordered_json> {}(ordered_json(0)) == std::hash<ordered_json> {}(ordered_json(static_cast<unsigned>(0)))); | |
| CHECK(std::hash<ordered_json> {}(ordered_json(0)) == std::hash<ordered_json> {}(ordered_json(0.0))); | |
| } | |
| TEST_CASE("hash of deeply nested values") | |
| { | |
| SECTION("hashing past the descent bound computes the same values") | |
| { | |
| // every depth on either side of where the iterative path takes over | |
| for (std::size_t depth = 0; depth <= (2 * nlohmann::detail::recursion_depth_limit()) + 10; ++depth) | |
| { | |
| CAPTURE(depth) | |
| const auto arrays = nested<json>(depth, false); | |
| const auto objects = nested<json>(depth, true); | |
| const auto ordered = nested<ordered_json>(depth, true); | |
| CHECK(std::hash<json> {}(arrays) == reference_hash(arrays)); | |
| CHECK(std::hash<json> {}(objects) == reference_hash(objects)); | |
| CHECK(std::hash<ordered_json> {}(ordered) == reference_hash(ordered)); | |
| } | |
| } | |
| SECTION("values nested too deeply for the call stack (#5545)") | |
| { | |
| // recursing once per level used to exhaust the call stack here; the | |
| // values are only parsed and hashed, never copied or compared, since | |
| // those recurse as well | |
| const std::size_t depth = 100000; | |
| for (const bool objects : | |
| { | |
| false, true | |
| }) | |
| { | |
| CAPTURE(objects) | |
| const auto text = nested_text(depth, objects); | |
| const auto a = json::parse(text); | |
| const auto b = json::parse(text); | |
| CHECK(std::hash<json> {}(a) == std::hash<json> {}(b)); | |
| const auto c = ordered_json::parse(text); | |
| const auto d = ordered_json::parse(text); | |
| CHECK(std::hash<ordered_json> {}(c) == std::hash<ordered_json> {}(d)); | |
| } | |
| } | |
| } |