Skip to content

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

Description

@nlohmann

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind: bugsolution: proposed fixa fix for the issue has been proposed and waits for confirmation

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions