nlohmann / nlohmann/json

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

Open
#5,400 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

kind: bug solution: proposed fix
Dominant language
C++
Stars
50.6k
Forks
7.5k
Avg merge
4d 17h
Merged PRs (30d)
58

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with include/nlohmann/detail/hash.hpp and compare its numeric handling with operator== in include/nlohmann/json.hpp. Reproduce the issue using the unordered_set example, then review docs/mkdocs/docs/api/basic_json/std_hash.md. Done means equal numeric values produce equal hashes, the set contains one element, and the documented behavior is updated.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.