Classify leaves with operator<=> itself past the nesting bound - #5686
Merged
Merged
Conversation
In C++20, an ordered comparison past the nesting bound classified a pair of leaves by asking == first and then order_leaves(), which calls < and > - both derived from <=>. For a pair of binary values with the same bytes but a different subtype, == reports them unequal, while <=> (through std::vector<std::uint8_t>::operator<=>) reports them equivalent, so the pair ended the comparison as unordered instead of letting the next element decide - unlike an array or object within the bound, which compares such a pair with its own operator<=> and gets equivalent. So operator<=>, and the <, <=, >, >= derived from it, could give a different result for the same two values depending on how deeply the values were nested, or unordered at every depth with JSON_NO_THREAD_LOCAL defined. compare_leaves() now classifies such a pair in C++20 with operator<=> itself instead, matching how a value within the bound is compared; the equality-only and pre-C++20 ordered cases are unchanged. Which of the three runs is chosen by overloading on std::integral_constant<bool, Ordered>, the same tag dispatch order_leaves() already uses, rather than a runtime "if (Ordered)" on a template parameter, which MSVC would flag as a constant condition (C4127). Added a regression test to unit-comparison.cpp that nests such a pair 0, 127, 128 and 200 levels deep (127 stays within the 128-level bound, 128 and 200 do not) and checks that operator<=> and operator< agree at every depth. Fixes #5654. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
gregmarr
reviewed
Sep 30, 2026
| ## Version history | ||
|
|
||
| 1. Added in version 3.11.0. | ||
| 1. Added in version 3.11.0. Fixed in version 3.13.0 to keep the result independent of nesting depth: before, a pair |
Contributor
There was a problem hiding this comment.
Did this bug exist in 3.11.0 or was it just added in the current work?
Owner
Author
gregmarr
approved these changes
Oct 4, 2026
nlohmann
added a commit
that referenced
this pull request
Oct 4, 2026
- binary_reader: rename the error_handler constructor parameter, which shadowed the member (-Wshadow, -Wshadow-field-in-constructor; #5746) - basic_json(copy_construct_tag, ...): declare it noexcept when copying the base class is (GCC 16 -Wnoexcept; #5690) - the scalar-on-left legacy comparison operators: noexcept only when converting the scalar is, like their member counterparts (#5682, #5751) - compare_leaves: use std::is_eq/is_lt/is_gt instead of comparing a std::partial_ordering with 0 (-Wzero-as-null-pointer-constant; #5686) - serializer: silence MSVC C4127 for the EnsureAscii template parameter (#5741, #5746) - clang-tidy: return the sanitized reference in binary_writer, take the key of ordered_map::find_impl by const reference (#5727), and mark the switches over parse_array_index (#5728) - ordered_map: keep <memory> for std::allocator (IWYU) Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
added a commit
that referenced
this pull request
Oct 4, 2026
* Keep the serializer conversion for objects whose keys cannot be converted #5591 added a test converting nlohmann::json into a basic_json whose string type cannot be constructed from std::string. That instantiates convert_iteratively(), whose members.emplace_back(next.key(), ...) needs exactly that key conversion, and broke the build of unit-alt-string. Dispatch on the key's constructibility and leave such conversions to the serializers, as the levels above the nesting bound already do (#3425). Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix the remaining CI failures on develop - unit-wstring: with a 16-bit wchar_t (Windows), a lone surrogate is reported as the ill-formed byte 0xFF since #5704; the std::wstring expectations still had the previous <U+0000>. - ci_single_binaries: json_literals.hpp (#5610) and json.hpp include each other on purpose, and IWYU, not following the cycle, asks to replace json.hpp with json_fwd.hpp. Report its findings without failing the build, as already done for json.hpp. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix the library warnings and noexcept specifications from the merged PRs - binary_reader: rename the error_handler constructor parameter, which shadowed the member (-Wshadow, -Wshadow-field-in-constructor; #5746) - basic_json(copy_construct_tag, ...): declare it noexcept when copying the base class is (GCC 16 -Wnoexcept; #5690) - the scalar-on-left legacy comparison operators: noexcept only when converting the scalar is, like their member counterparts (#5682, #5751) - compare_leaves: use std::is_eq/is_lt/is_gt instead of comparing a std::partial_ordering with 0 (-Wzero-as-null-pointer-constant; #5686) - serializer: silence MSVC C4127 for the EnsureAscii template parameter (#5741, #5746) - clang-tidy: return the sanitized reference in binary_writer, take the key of ordered_map::find_impl by const reference (#5727), and mark the switches over parse_array_index (#5728) - ordered_map: keep <memory> for std::allocator (IWYU) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Split unit-conversions.cpp so MinGW can link it clang 18 with the MinGW linker failed to link test-conversions_cpp17 ("relocation truncated to fit: IMAGE_REL_AMD64_REL32"). As windows.yml recommends, keep the objects small by splitting the test file. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix the tests added by the merged PRs for all CI configurations - discard the results of dump() and from_*() in CHECK_THROWS with utils::ignore_return_value (GCC -Werror=unused-result) - give unit-bson's huge_string_t a default constructor (MSVC C2512, GCC 5, clang 3.5) - unit-disabled_exceptions: use the literals namespace when the global UDLs are off (ci_test_noglobaludls; #5700) - unit-binary_utf8_strict: expect the JSON pointer prefix with JSON_DIAGNOSTICS (#5741) - skip the tests that rely on exceptions under JSON_NOEXCEPTION (#5678, #5732) - clang-tidy and clang -Werror: static test data, CAPTURE(...);, const-correctness, use-after-move alias, unused conversion operator, a missing <iterator> include Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Title the macro examples and add JSON_STRICT_BINARY_UTF8 to the docset The documentation style check requires "Example: ..." titles on pages with several examples (#5741, #5591) and a docset entry for every macro page. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Regenerate BUILD.bazel and nlohmann_json.natvis Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5746 added detail/output/error_handler.hpp and #5741 the json_abi_sbu8 ABI tag. * Install libidn11 for the CMake 3.5.0 binary in ci_cmake_flags Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5733 moved ci_cmake_options from ubuntu:focal to ubuntu:24.04, which no longer ships libidn.so.11; the CMake 3.5.0 release binary links against it, so every ci_cmake_flags run has failed since. Install focal's libidn11 package for that matrix entry only. * Suppress Infer's false STACK_VARIABLE_ADDRESS_ESCAPE in get_impl get_impl() returns its local by value. A test added by the merged PRs instantiates it with a type Infer misreads, so ci_infer reported the 2021 code for the first time. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In C++20,
operator<=>(and the<,<=,>,>=derived from it) could give a differentresult for the same two values depending on how deeply they are nested. This happens when the
values contain binary values with the same bytes but a different subtype:
operator==comparesthe subtype and reports them unequal, but
operator<=>compares only the bytes (throughstd::vector<std::uint8_t>::operator<=>) and reports them equivalent. Within the first 128levels, an array or object compares itself with
operator<=>and lets the next element decide.Past that bound,
compare_iteratively<true>()classified a leaf pair by asking==first andthen
</>(both derived from<=>), so the pair ended the comparison asunorderedinstead -and at every depth with
JSON_NO_THREAD_LOCALdefined. This is a regression from #5390.Changes
compare_leaves()ininclude/nlohmann/json.hppnow classifies a leaf pair for an orderedC++20 comparison with
operator<=>itself, matching how a value within the nesting bound iscompared. The equality-only and pre-C++20 ordered cases are unchanged.
std::integral_constant<bool, Ordered>, the tag dispatchorder_leaves()already uses,rather than a runtime
if (Ordered)on a template parameter, which MSVC flags as a constantcondition (C4127).
single_include/nlohmann/json.hppregenerated withmake amalgamate.Tests
Added a regression test to
tests/src/unit-comparison.cpp(C++20 only) that nests a pair ofarrays whose first elements are binary values with the same bytes but a different subtype, 0,
127, 128 and 200 levels deep (127 stays within the 128-level nesting bound, 128 and 200 do not),
and checks that
operator<=>andoperator<agree at every depth.develop's headers (633de8e),it fails at 128 and 200 levels exactly as the issue describes, and passes at 0 and 127.
-DJSON_NO_THREAD_LOCAL, under-fsanitize=address,undefined -fno-sanitize-recover=undefinedwith-Wall -Wextra -Werror.Public API
No breaking changes.
compare_leaves()is a private implementation detail; the observablechange is that
operator<=>'s result (and the operators derived from it) for values nested past128 levels now matches the documented and already-intended behavior of comparing "using their
respective
operator<=>" at every depth.Fixes #5654
This PR was written by Claude Code on behalf of @nlohmann.
🤖 Generated with Claude Code