Skip to content

Fix integer comparison bug#5211

Open
ljcjclljc wants to merge 9 commits into
nlohmann:developfrom
ljcjclljc:issue5210
Open

Fix integer comparison bug#5211
ljcjclljc wants to merge 9 commits into
nlohmann:developfrom
ljcjclljc:issue5210

Conversation

@ljcjclljc

Copy link
Copy Markdown

This PR provides a minimal implementation for #5210.
Fiex #5210

Background

When comparing number_unsigned and number_integer, the current implementation converts the unsigned value to number_integer_t before applying the operator.

This is problematic for values above INT64_MAX, because converting such uint64_t values to number_integer_t changes them into the negative signed range. As a result, valid mixed signed/unsigned comparisons can return mathematically incorrect results.

Problem

The affected logic is in the mixed comparison branches of JSON_IMPLEMENT_OPERATOR in include/nlohmann/json.hpp.

Before this change, comparisons such as these could fail:

  • json(-1) < json(18446744073709551615ULL) returned false
  • json(18446744073709551615ULL) == json(-1) returned true

This is not limited to one construction path. The same incorrect behavior can be observed after direct construction, object assignment, array insertion, initializer-list construction, nested access, and json::parse().

Solution

This PR keeps the existing macro shape and applies the smallest possible fix in the mixed number_unsigned / number_integer comparison branches:

  • If the signed operand is negative, return the mathematically correct ordering result directly.
  • If the signed operand is non-negative, cast it to number_unsigned_t and compare in the unsigned domain.

This avoids the unsafe uint64_t -> int64_t conversion while preserving the existing comparison structure.

Scope

This PR is intentionally minimal:

  • no refactor of the comparison macro
  • no change to the public API
  • no unrelated behavior changes

It only fixes the signed/unsigned mixed comparison bug described in #5210.

  • The changes are described in detail, both the what and why.
  • If applicable, an https://github.com/nlohmann/json/issues is referenced.
  • The https://coveralls.io/github/nlohmann/json remained at 100%. A test case for every new line of code.
  • If applicable, the https://json.nlohmann.me is updated.
  • The source code is amalgamated by running make amalgamate.

@github-actions

Copy link
Copy Markdown

🔴 Amalgamation check failed! 🔴

The source code has not been amalgamated. @ljcjclljc
Please read and follow the Contribution Guidelines.

@gregmarr gregmarr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems reasonable at a low level. There may be some cases that should be documented where a < b and json(a) < json(b) will provide different results. Those are the same cases where the compiler should have been warning you not to do that a < b, so that's probably okay, but good to call it out explicitly.

Comment thread include/nlohmann/json.hpp Outdated
return lhs.m_data.m_value.number_float op static_cast<number_float_t>(rhs.m_data.m_value.number_unsigned); \
} \
else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) \
else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You lost a space here.

Comment thread include/nlohmann/json.hpp Outdated
{ \
return number_integer_t(1) op number_integer_t(0); \
} \
return lhs.m_data.m_value.number_unsigned op static_cast<number_unsigned_t>(rhs.m_data.m_value.number_integer); \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good change, aligns more with the C++ promotion rules.

ljcjclljc and others added 4 commits June 18, 2026 11:02
Signed-off-by: ljccjlljc <939159710@qq.com>
Signed-off-by: ljccjlljc <939159710@qq.com>
…r std_fs::path (nlohmann#5209)

* added explicit instantiations of 'has_from_json' and 'has_to_json' for std_fs::path;
Signed-off-by: drcosmin <cosmin.dr@pm.me>

* Fixed amalgamation

Signed-off-by: drcosmin <cosmin.dr@pm.me>

---------

Signed-off-by: drcosmin <cosmin.dr@pm.me>
Signed-off-by: ljccjlljc <939159710@qq.com>
Signed-off-by: ljccjlljc <939159710@qq.com>
Signed-off-by: ljccjlljc <939159710@qq.com>
@github-actions

Copy link
Copy Markdown

🔴 Amalgamation check failed! 🔴

The source code has not been amalgamated. @ljcjclljc
Please read and follow the Contribution Guidelines.

Signed-off-by: ljccjlljc <939159710@qq.com>
Comment thread tests/src/uint64_compare_test.cpp

@gregmarr gregmarr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this needs a lot more documentation, both in the main documentation section, and somewhere that will show up in the changes section of the readme as a potentially breaking change, with the appropriate rationale, as described in the linked issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be part of this PR, or separate? I am leaning towards separate.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please separate this from the original PR scope.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is already on develop branch. Please rebase.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be part of this PR, or separate? I am leaning towards separate.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please separate this from the original PR scope.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is already on develop branch. Please rebase.

Comment thread include/nlohmann/json.hpp Outdated
return lhs.m_data.m_value.number_unsigned op static_cast<number_unsigned_t>(rhs.m_data.m_value.number_integer); \
} \
else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) \
else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) /* NOLINT(readability/braces) */ \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than disabling the lint warning, why not just add the parens and make it clear what the precedence is?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please fix this.

Comment thread include/nlohmann/json.hpp Outdated
// Mixed signed/unsigned integer comparisons check for negative signed
// values before casting. This avoids wraparound when converting a negative
// integer to an unsigned type and preserves the rule that any negative
// integer is smaller than any unsigned integer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we probably don't want to put a comment inside the macro explaining the actual op, should there be one here?

  // This is done by checking for the signed value being less than zero.
  // If it is, the comparison is performed using `0` for the signed value
  // and `1` for the unsigned value.  Since the negative signed value will
  // always be less than the unsigned value, any two fixed values that 
  // have the same relationship will produce the right result.
  // If the signed value is not less than zero, it is cast to unsigned
  // before doing the comparison.

I'm wondering if -1 and 1 would be clearer than 0 and 1. That way you can explicitly see the negative sign.

Comment thread include/nlohmann/json.hpp Outdated
return lhs.m_data.m_value.number_float op static_cast<number_float_t>(rhs.m_data.m_value.number_unsigned); \
} \
else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) \
else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) /* NOLINT(readability/braces) */ \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than disabling the lint warning, why not just add the parens and make it clear what the precedence is?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please fix this.

@ljcjclljc ljcjclljc requested a review from nlohmann July 4, 2026 14:01

@nlohmann nlohmann left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments. Will review later.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please separate this from the original PR scope.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please separate this from the original PR scope.

Comment thread include/nlohmann/json.hpp Outdated
return lhs.m_data.m_value.number_float op static_cast<number_float_t>(rhs.m_data.m_value.number_unsigned); \
} \
else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) \
else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) /* NOLINT(readability/braces) */ \

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please fix this.

Comment thread include/nlohmann/json.hpp Outdated
return lhs.m_data.m_value.number_unsigned op static_cast<number_unsigned_t>(rhs.m_data.m_value.number_integer); \
} \
else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) \
else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) /* NOLINT(readability/braces) */ \

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please fix this.

Signed-off-by: ljccjlljc <939159710@qq.com>
@github-actions github-actions Bot removed the M label Jul 4, 2026
Signed-off-by: ljccjlljc <939159710@qq.com>
@github-actions github-actions Bot added the M label Jul 4, 2026
@@ -0,0 +1,19 @@
#include <cstdint>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file can now be removed given we have tests/src/unit-comparison.cpp.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is already on develop branch. Please rebase.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is already on develop branch. Please rebase.

const json max_uint64 = (std::numeric_limits<std::uint64_t>::max)();
const json negative_one = -1;

CHECK_FALSE(above_int64_max == negative_one);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests only check == and < but not <=, >, >=, !=, or operator<=>, and don't cover the non‑negative‑signed + large‑unsigned path. Please add these.

@nlohmann nlohmann added the please rebase Please rebase your branch to origin/develop label Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

M please rebase Please rebase your branch to origin/develop tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mixed comparison bug: number_unsigned (INT64_MAX+1~UINT64_MAX) vs number_integer

4 participants