std::hash<basic_json> is inconsistent with operator== for equal cross-type numbers (breaks unordered containers)
Description
std::hash<basic_json> can produce different hash values for two json values that compare equal under operator==. The C++ standard requires of a Hash used with std::unordered_map/unordered_set that a == b implies hash(a) == hash(b); this invariant is violated for numbers of different internal types.
operator== converts between number_integer, number_unsigned, and number_float before comparing (include/nlohmann/json.hpp, JSON_IMPLEMENT_OPERATOR), so json(0) == json(0.0) == json(0u) are all true. But hash() folds the value_t type tag into the result via combine(type, h) (include/nlohmann/detail/hash.hpp), giving each of the three a distinct hash.
Consequently a std::unordered_set<json> can hold json(0), json(0.0), and json(0u) as three separate elements even though they are all equal — silent duplicate keys / lookup misses.
The documentation (std_hash.md) currently describes this as intended ("different hash values for null, 0, 0U, and false"). But 0, 0U, and 0.0 are equal under operator==, so distinguishing them by hash is precisely the contract violation. Only false vs 0 is legitimately distinguishable (those are not equal — different value_t and no cross-conversion). This looks like a case where the documented intent itself encodes the bug.
Reproduction steps
Hash json(0), json(0.0), json(0u); observe they are == but hash differently and coexist in an unordered_set.
Expected vs. actual results
- Expected: equal values hash equally;
unordered_setof{0, 0.0, 0u}has size 1. - Actual: three distinct hashes;
unordered_setsize is 3.
Minimal code example
#include <nlohmann/json.hpp>
#include <unordered_set>
#include <cstdio>
using json = nlohmann::json;
int main()
{
json a = 0, b = 0.0, c = 0u;
std::hash<json> H;
std::printf("a==b:%d a==c:%d hashes: %zu %zu %zu\n",
(int)(a==b), (int)(a==c), H(a), H(b), H(c));
std::unordered_set<json> s; s.insert(a); s.insert(b); s.insert(c);
std::printf("unordered_set size (all == 0): %zu (expected 1)\n", s.size());
}
Error messages
a==b:1 a==c:1 hashes: 2654436095 2654436221 2654436156
unordered_set size (all == 0): 3 (expected 1)
Discussion / possible fix
Make hash() consistent with operator== for numbers, e.g. normalize numeric values to a canonical representation before hashing (so that any two numerically-equal values hash the same), or fold a single "number" category into the hash instead of the specific value_t. This is a semantics decision for the maintainer; the current behavior breaks the standard Hash requirement whenever numeric json values are used as keys in unordered containers.
Compiler and operating system
g++ 13.3.0 (Ubuntu 24.04, x86-64)
Library version
develop @ 01853ed6bcf9ebe88ec2e248ea757b86417b1487
Validation
- The bug also occurs if the latest version from the
developbranch is used. - I can successfully compile and run the unit tests.
Source: nlohmann/json