mirror of
https://github.com/nlohmann/json.git
synced 2026-10-08 15:37:13 +00:00
* Make std::hash<basic_json> consistent with operator== for numbers operator== converts between number_integer, number_unsigned, and number_float before comparing, so json(0), json(0U), and json(0.0) all compare equal. hash() folded the specific value_t into the result for each of the three numeric cases, giving each a distinct hash and breaking the standard Hash requirement that a == b implies hash(a) == hash(b). A std::unordered_set could therefore hold all three as separate elements even though they compare equal. hash() now treats all three numeric variants the same way: it converts the value to number_float_t and combines it with a single shared type tag, so any two numbers operator== considers equal hash identically regardless of which internal type actually holds them. Updated the accompanying test to check this consistency directly (including via an actual unordered_set) instead of asserting that 0, 0U, and 0.0 hash differently, since that assumption was the bug. Also corrected the function's own doc comment and the std::hash API docs, which described the old behavior as intended. Fixes #5400 Signed-off-by: Afonso Januário <afonso-januario@hotmail.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove now-unused number_integer_t/number_unsigned_t typedefs in hash() Merging the three numeric branches into one that only reads number_float_t left these two aliases unused, which several CI configurations treat as a build error under -Wunused-local-typedefs. Signed-off-by: Afonso Januário <afonso-januario@hotmail.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Mark the unordered_set in the hash regression test const clang-tidy's misc-const-correctness check flagged it: the set is never mutated after construction, only read via size(). Signed-off-by: Afonso Januário <afonso-januario@hotmail.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Normalize -0.0 in number hashes and test range ends operator== compares numbers exactly since #5459, so equal numbers share one value and convert to the same number_float_t. Update the comment accordingly, map -0.0 to 0.0 before hashing (std::hash need not do that), and test -0.0 and the ends of the integer ranges. Show hash(0.0) in the docs example and note the change in the version history. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Clarify hash documentation after review - Say "may hash differently" for null, false, and numbers, since a collision across types is possible. - Name the storage types (signed integer, unsigned integer, floating-point number) instead of example literals. - Explain that the hash survives converting an integer to number_float_t but not the lossy conversion back, and that unequal numbers may share a hash. - State that the example hash values are illustrative only and vary by platform, compiler, compiler version, and library version. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Afonso Januário <afonso-januario@hotmail.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me> Co-authored-by: Afonso Januário <afonso-januario@hotmail.com>
258 lines
9.4 KiB
C++
258 lines
9.4 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | 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 json = nlohmann::json;
|
|
using ordered_json = 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 = 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(json::number_unsigned_t(int_max)) == json(int_max));
|
|
CHECK(std::hash<json> {}(json(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));
|
|
}
|
|
}
|
|
}
|