#5400·json

std::hash<basic_json> is inconsistent with operator== for equal cross-type numbers (breaks unordered containers)

Author: nlohmannCreated Aug 25, 2026Updated Sep 5, 2026
Labelskind: bugsolution: proposed fix

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_set of {0, 0.0, 0u} has size 1.
  • Actual: three distinct hashes; unordered_set size 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 develop branch is used.
  • I can successfully compile and run the unit tests.