Files
json/tests/src/unit-comparison.cpp
T
Niels Lohmann b38131ec40 Add JSON_RECURSION_DEPTH_LIMIT to set how deep value walks recurse
Copying, comparing, serializing, hashing, merging, diffing, converting and
destroying a value, and the CBOR, MessagePack, UBJSON, BJData and BON8 writers
recurse a fixed number of nesting levels on the call stack and finish deeper
levels with an explicit stack (#5387). The number was hard-coded to 128. That is
too much for small stacks: a 128-level dump() needs about 288 KiB at -O0 with
Clang (2.3 KiB per level; 256 KiB with GCC) and crashed in a thread with a
256 KiB stack, and musl threads have 128 KiB by default. Others may want the
opposite.

The macro JSON_RECURSION_DEPTH_LIMIT, defined before including the library,
sets the number; the default stays 128. The value must be an integer constant
from 0 to 254 (not a floating-point number or bool), otherwise a static_assert
names the macro. 254 is the largest value because the copy constructor and the
comparison operators cannot pass a depth down and count it in a byte per thread
(nesting_depth()). The guard that counts now counts only the levels it enters,
so the count never exceeds the limit. It used to count the level that finds the
limit reached as well, which made the count 255 at limit 254: a copy made by a
custom base class while a copy was going on then wrapped the byte to 0 and
recursed another 254 levels (twice the stack).

0 is allowed: every walk then uses its explicit stack from the first level on
and needs a constant amount of stack (16 KiB or less at
-O0 with Clang and GCC, for a value nested 20,000 levels deep). All uses of the
limit compare a depth with it; none computes limit - 1 or limit + 1, and no
walk has a constant 128 of its own.

The macro changes only stack use and speed, never a result. Like
JSON_NO_THREAD_LOCAL it is not part of the ABI tag and must be the same in all
translation units (ODR); the page of the macro says what happens otherwise.

Supporting a limit of 0 needed one fix: update() without merge_objects reached
the explicit-stack merge at depth 0, which always merges objects. update_members
now takes that path only with merge_objects, which is the only case in which it
recurses at all.

Measurements (macOS arm64, Apple clang 21 and g++ 16, documented in the macro
page with a table per operation and optimization level, and a program to
reproduce them): the stack needed at the default of 128 levels is up to 288 KiB
(dump at -O0, Clang), 224 KiB (copy), and 48 KiB or less at -O2; at 254 levels
up to 576 KiB at -O0 and 112 KiB at -O2. Speed at -O2 relative to the default:
ordinary documents (nested 2 to 10 levels) are 1.1 to 2.4 times as slow with a
limit of 0 or 1 and unchanged (within about 0.1) with 16 or 254; values nested
1,000 or 100,000 levels deep show no difference for any limit. The default
build does the same work as before: the instructions retired for copy and conversion
(the walks whose guard changed; net of loading, canada, citm, twitter, jeopardy
and deep values), ==, dump and hash differ from the base by at most 0.02 % for copy
and 0.71 % in either direction for the others (noise), and the cycles for copy
by at most 0.65 %.

Tests: the existing tests that depended on 128 (comparison, allocator, custom
base class, diagnostics, binary writers, patch, merge_diff, conversions, custom
array type) derive their depths from the limit, and two assumed a limit of at
least 1 (limit - 1 wrapped around). tests/src/unit-recursion_depth_limit.cpp
checks that the macro is honoured (the array type of a value counts the levels
that a copy and a comparison descend) and that all walks return the same results
for values nested 1 to 300 levels deep as arrays, objects and both, against
expected strings and bytes built independently of the library; CMake builds it
with limits 0, 1 and 254 besides the default, and the CI target
ci_test_recursion_depth_limit_0 runs the whole suite with a limit of 0. A test
with a base class that copies the value from its copy constructor covers the
wrap-around at 254. The new test does not compile against the
previous header with -DJSON_RECURSION_DEPTH_LIMIT=1.

Docs: new macro page (range, stack and speed tables, small-stack guidance, ODR),
index, nav, macro overview, "Nesting depth" of the untrusted-input page (which
claimed that no walk could exhaust the stack however deep a value is nested; the
default assumes a stack of a few hundred KiB), the assurance case, and the page
of JSON_NO_THREAD_LOCAL.

No breaking changes: the default is unchanged, and the macro is new.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-11 18:32:55 +01:00

1317 lines
57 KiB
C++

// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// cmake/test.cmake selects the C++ standard versions with which to build a
// unit test based on the presence of JSON_HAS_CPP_<VERSION> macros.
// When using macros that are only defined for particular versions of the standard
// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding
// version macro in a comment close by, like this:
// JSON_HAS_CPP_<VERSION> (do not remove; see note at top of file)
#include "doctest_compatibility.h"
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <limits>
#include <map>
#include <string>
#include <utility>
#include <vector>
#define JSON_TESTS_PRIVATE
#include <nlohmann/json.hpp>
using nlohmann::json;
#if JSON_HAS_THREE_WAY_COMPARISON
// this can be replaced with the doctest stl extension header in version 2.5
namespace doctest
{
template<> struct StringMaker<std::partial_ordering>
{
static String convert(const std::partial_ordering& order)
{
if (order == std::partial_ordering::less)
{
return "std::partial_ordering::less";
}
if (order == std::partial_ordering::equivalent)
{
return "std::partial_ordering::equivalent";
}
if (order == std::partial_ordering::greater)
{
return "std::partial_ordering::greater";
}
if (order == std::partial_ordering::unordered)
{
return "std::partial_ordering::unordered";
}
return "{?}";
}
};
} // namespace doctest
#endif
namespace
{
// nesting depths on either side of detail::recursion_depth_limit(): none, the
// last one within the limit, the first one past it, and well past it (0, 127,
// 128, and 200 by default)
std::vector<std::size_t> depths_around_recursion_limit()
{
const std::size_t limit = nlohmann::detail::recursion_depth_limit();
return {0, limit > 0 ? limit - 1 : 0, limit, limit + 72};
}
// helper function to check std::less<json::value_t>
// see https://en.cppreference.com/w/cpp/utility/functional/less
template <typename A, typename B, typename U = std::less<json::value_t>>
bool f(A a, B b, U u = U())
{
return u(a, b);
}
} // namespace
TEST_CASE("lexicographical comparison operators")
{
constexpr auto f_ = false;
constexpr auto _t = true;
constexpr auto nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
#if JSON_HAS_THREE_WAY_COMPARISON
constexpr auto lt = std::partial_ordering::less;
constexpr auto gt = std::partial_ordering::greater;
constexpr auto eq = std::partial_ordering::equivalent;
constexpr auto un = std::partial_ordering::unordered;
#endif
#if JSON_HAS_THREE_WAY_COMPARISON
INFO("using 3-way comparison");
#endif
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
INFO("using legacy comparison");
#endif
//REQUIRE(std::numeric_limits<json::number_float_t>::has_quiet_NaN);
REQUIRE(std::isnan(nan));
SECTION("types")
{
std::vector<json::value_t> j_types =
{
json::value_t::null,
json::value_t::boolean,
json::value_t::number_integer,
json::value_t::number_unsigned,
json::value_t::number_float,
json::value_t::object,
json::value_t::array,
json::value_t::string,
json::value_t::binary,
json::value_t::discarded
};
std::vector<std::vector<bool>> expected_lt =
{
//0 1 2 3 4 5 6 7 8 9
{f_, _t, _t, _t, _t, _t, _t, _t, _t, f_}, // 0
{f_, f_, _t, _t, _t, _t, _t, _t, _t, f_}, // 1
{f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 2
{f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 3
{f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 4
{f_, f_, f_, f_, f_, f_, _t, _t, _t, f_}, // 5
{f_, f_, f_, f_, f_, f_, f_, _t, _t, f_}, // 6
{f_, f_, f_, f_, f_, f_, f_, f_, _t, f_}, // 7
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 8
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 9
};
SECTION("comparison: less")
{
REQUIRE(expected_lt.size() == j_types.size());
for (size_t i = 0; i < j_types.size(); ++i)
{
REQUIRE(expected_lt[i].size() == j_types.size());
for (size_t j = 0; j < j_types.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
// check precomputed values
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
CHECK((j_types[i] < j_types[j]) == expected_lt[i][j]);
#else
CHECK(operator<(j_types[i], j_types[j]) == expected_lt[i][j]);
#endif
CHECK(f(j_types[i], j_types[j]) == expected_lt[i][j]);
}
}
}
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
SECTION("comparison: 3-way")
{
std::vector<std::vector<std::partial_ordering>> expected =
{
//0 1 2 3 4 5 6 7 8 9
{eq, lt, lt, lt, lt, lt, lt, lt, lt, un}, // 0
{gt, eq, lt, lt, lt, lt, lt, lt, lt, un}, // 1
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 2
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 3
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 4
{gt, gt, gt, gt, gt, eq, lt, lt, lt, un}, // 5
{gt, gt, gt, gt, gt, gt, eq, lt, lt, un}, // 6
{gt, gt, gt, gt, gt, gt, gt, eq, lt, un}, // 7
{gt, gt, gt, gt, gt, gt, gt, gt, eq, un}, // 8
{un, un, un, un, un, un, un, un, un, un}, // 9
};
// check expected partial_ordering against expected boolean
REQUIRE(expected.size() == expected_lt.size());
for (size_t i = 0; i < expected.size(); ++i)
{
REQUIRE(expected[i].size() == expected_lt[i].size());
for (size_t j = 0; j < expected[i].size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]);
}
}
// check 3-way comparison against expected partial_ordering
REQUIRE(expected.size() == j_types.size());
for (size_t i = 0; i < j_types.size(); ++i)
{
REQUIRE(expected[i].size() == j_types.size());
for (size_t j = 0; j < j_types.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CHECK((j_types[i] <=> j_types[j]) == expected[i][j]); // *NOPAD*
}
}
}
#endif
}
SECTION("values")
{
json j_values =
{
nullptr, nullptr, // 0 1
-17, 42, // 2 3
8u, 13u, // 4 5
3.14159, 23.42, // 6 7
nan, nan, // 8 9
"foo", "bar", // 10 11
true, false, // 12 13
{1, 2, 3}, {"one", "two", "three"}, // 14 15
{{"first", 1}, {"second", 2}}, {{"a", "A"}, {"b", {"B"}}}, // 16 17
json::binary({1, 2, 3}), json::binary({1, 2, 4}), // 18 19
json(json::value_t::discarded), json(json::value_t::discarded) // 20 21
};
std::vector<std::vector<bool>> expected_eq =
{
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
{_t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 0
{_t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 1
{f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 2
{f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 3
{f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 4
{f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 5
{f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 6
{f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 7
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 8
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 9
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 10
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 11
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 12
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_}, // 13
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_}, // 14
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_}, // 15
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_}, // 16
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_}, // 17
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_}, // 18
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_}, // 19
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 20
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 21
};
std::vector<std::vector<bool>> expected_lt =
{
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_}, // 0
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_}, // 1
{f_, f_, f_, _t, _t, _t, _t, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 2
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 3
{f_, f_, f_, _t, f_, _t, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 4
{f_, f_, f_, _t, f_, f_, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 5
{f_, f_, f_, _t, _t, _t, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 6
{f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 7
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 8
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 9
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 10
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 11
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 12
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 13
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, _t, f_, f_, _t, _t, f_, f_}, // 14
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 15
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, f_, f_, _t, _t, f_, f_}, // 16
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, f_, _t, _t, f_, f_}, // 17
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_}, // 18
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 19
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 20
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 21
};
SECTION("signed/unsigned mixed comparison above INT64_MAX")
{
const json above_int64_max = static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1ULL;
const json max_uint64 = (std::numeric_limits<std::uint64_t>::max)();
const json negative_one = -1;
const json one = 1;
const json max_int64 = (std::numeric_limits<std::int64_t>::max)();
CHECK_FALSE(above_int64_max == negative_one);
CHECK(above_int64_max != negative_one);
CHECK(negative_one < above_int64_max);
CHECK(negative_one <= above_int64_max);
CHECK_FALSE(negative_one > above_int64_max);
CHECK_FALSE(negative_one >= above_int64_max);
CHECK_FALSE(above_int64_max < negative_one);
CHECK_FALSE(above_int64_max <= negative_one);
CHECK(above_int64_max > negative_one);
CHECK(above_int64_max >= negative_one);
CHECK(negative_one != above_int64_max);
CHECK_FALSE(negative_one == above_int64_max);
CHECK_FALSE(max_uint64 == negative_one);
CHECK(max_uint64 != negative_one);
CHECK(negative_one < max_uint64);
CHECK(negative_one <= max_uint64);
CHECK_FALSE(negative_one > max_uint64);
CHECK_FALSE(negative_one >= max_uint64);
CHECK_FALSE(max_uint64 < negative_one);
CHECK_FALSE(max_uint64 <= negative_one);
CHECK(max_uint64 > negative_one);
CHECK(max_uint64 >= negative_one);
CHECK(negative_one != max_uint64);
CHECK_FALSE(negative_one == max_uint64);
CHECK_FALSE(one == above_int64_max);
CHECK(one != above_int64_max);
CHECK(one < above_int64_max);
CHECK(one <= above_int64_max);
CHECK_FALSE(one > above_int64_max);
CHECK_FALSE(one >= above_int64_max);
CHECK_FALSE(above_int64_max < one);
CHECK_FALSE(above_int64_max <= one);
CHECK(above_int64_max > one);
CHECK(above_int64_max >= one);
CHECK_FALSE(max_int64 == above_int64_max);
CHECK(max_int64 != above_int64_max);
CHECK(max_int64 < above_int64_max);
CHECK(max_int64 <= above_int64_max);
CHECK_FALSE(max_int64 > above_int64_max);
CHECK_FALSE(max_int64 >= above_int64_max);
CHECK_FALSE(above_int64_max < max_int64);
CHECK_FALSE(above_int64_max <= max_int64);
CHECK(above_int64_max > max_int64);
CHECK(above_int64_max >= max_int64);
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
CHECK((negative_one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
CHECK((above_int64_max <=> negative_one) == std::partial_ordering::greater); // *NOPAD*
CHECK((negative_one <=> max_uint64) == std::partial_ordering::less); // *NOPAD*
CHECK((max_uint64 <=> negative_one) == std::partial_ordering::greater); // *NOPAD*
CHECK((one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
CHECK((above_int64_max <=> one) == std::partial_ordering::greater); // *NOPAD*
CHECK((max_int64 <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
CHECK((above_int64_max <=> max_int64) == std::partial_ordering::greater); // *NOPAD*
#endif
}
SECTION("integer/float mixed comparison is exact")
{
// Widening the integer to a double loses precision past the
// mantissa, so 2^63-2 and 2^63-1 both used to compare equal to the
// double 2^63 while differing from each other. That makes equality
// intransitive and the ordering not a strict weak ordering.
const json below_two_63 = static_cast<std::int64_t>(9223372036854775806LL);
const json max_int64 = (std::numeric_limits<std::int64_t>::max)();
const json two_63 = 9223372036854775808.0;
CHECK_FALSE(below_two_63 == two_63);
CHECK_FALSE(max_int64 == two_63);
CHECK(below_two_63 != max_int64);
CHECK(below_two_63 < max_int64);
CHECK(below_two_63 < two_63);
CHECK(max_int64 < two_63);
CHECK(two_63 > max_int64);
CHECK_FALSE(two_63 < max_int64);
// the same past the unsigned range
const json max_uint64 = (std::numeric_limits<std::uint64_t>::max)();
const json two_64 = 18446744073709551616.0;
CHECK_FALSE(max_uint64 == two_64);
CHECK(max_uint64 < two_64);
CHECK(two_64 > max_uint64);
// values a double represents exactly still compare equal
CHECK(json(1) == json(1.0));
CHECK(json(1u) == json(1.0));
CHECK(json(-3) == json(-3.0));
CHECK(json(1) < json(1.5));
CHECK(json(1.5) < json(2));
CHECK(json(2) > json(1.5));
CHECK(json(-1) > json(-1.5));
CHECK(json(-1.5) < json(-1));
CHECK(json(-2) < json(-1.5));
// a float below the range of the integer type
CHECK(json(0) > json(-1e30));
CHECK(json(-1e30) < json(0));
CHECK(json(0u) > json(-0.5));
CHECK(json(-0.5) < json(0u));
// a NaN operand stays unordered against either integer kind
CHECK_FALSE(json(1) == json(nan));
CHECK_FALSE(json(1) < json(nan));
CHECK_FALSE(json(nan) < json(1));
CHECK_FALSE(json(1u) == json(nan));
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
CHECK((max_int64 <=> two_63) == std::partial_ordering::less); // *NOPAD*
CHECK((two_63 <=> max_int64) == std::partial_ordering::greater); // *NOPAD*
CHECK((below_two_63 <=> max_int64) == std::partial_ordering::less); // *NOPAD*
CHECK((max_uint64 <=> two_64) == std::partial_ordering::less); // *NOPAD*
CHECK((json(1) <=> json(1.0)) == std::partial_ordering::equivalent); // *NOPAD*
CHECK((json(1) <=> json(nan)) == std::partial_ordering::unordered); // *NOPAD*
#endif
}
SECTION("compares unordered")
{
std::vector<std::vector<bool>> expected =
{
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 0
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 1
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 2
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 3
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 4
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 5
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 6
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 7
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 8
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 9
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 10
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 11
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 12
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 13
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 14
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 15
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 16
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 17
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 18
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t}, // 19
{_t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t}, // 20
{_t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t}, // 21
};
// check if two values compare unordered as expected
REQUIRE(expected.size() == j_values.size());
for (size_t i = 0; i < j_values.size(); ++i)
{
REQUIRE(expected[i].size() == j_values.size());
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CHECK(json::compares_unordered(j_values[i], j_values[j]) == expected[i][j]);
}
}
}
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
SECTION("compares unordered (inverse)")
{
std::vector<std::vector<bool>> expected =
{
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 0
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 1
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 2
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 3
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 4
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 5
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 6
{f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 7
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 8
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 9
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 10
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 11
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 12
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 13
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 14
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 15
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 16
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 17
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 18
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 19
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 20
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 21
};
// check that two values compare unordered as expected (with legacy-mode enabled)
REQUIRE(expected.size() == j_values.size());
for (size_t i = 0; i < j_values.size(); ++i)
{
REQUIRE(expected[i].size() == j_values.size());
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CAPTURE(j_values[i])
CAPTURE(j_values[j])
CHECK(json::compares_unordered(j_values[i], j_values[j], true) == expected[i][j]);
}
}
}
#endif
SECTION("comparison: equal")
{
// check that two values compare equal
REQUIRE(expected_eq.size() == j_values.size());
for (size_t i = 0; i < j_values.size(); ++i)
{
REQUIRE(expected_eq[i].size() == j_values.size());
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CHECK((j_values[i] == j_values[j]) == expected_eq[i][j]);
}
}
// compare with null pointer
json j_null;
CHECK(j_null == nullptr);
CHECK(nullptr == j_null);
}
SECTION("comparison: not equal")
{
// check that two values compare unequal as expected
// operator!= now means exactly !(a==b) without special cases for NaN/discarded
for (size_t i = 0; i < j_values.size(); ++i)
{
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CHECK((j_values[i] != j_values[j]) == !(j_values[i] == j_values[j]));
}
}
// compare with null pointer
const json j_null;
CHECK((j_null != nullptr) == !(j_null == nullptr));
CHECK((nullptr != j_null) == !(nullptr == j_null));
}
SECTION("comparison: less")
{
// check that two values compare less than as expected
REQUIRE(expected_lt.size() == j_values.size());
for (size_t i = 0; i < j_values.size(); ++i)
{
REQUIRE(expected_lt[i].size() == j_values.size());
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CHECK((j_values[i] < j_values[j]) == expected_lt[i][j]);
}
}
}
SECTION("comparison: less than or equal equal")
{
// check that two values compare less than or equal as expected
for (size_t i = 0; i < j_values.size(); ++i)
{
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
if (json::compares_unordered(j_values[i], j_values[j], true))
{
// if two values compare unordered,
// check that the boolean comparison result is always false
CHECK_FALSE(j_values[i] <= j_values[j]);
}
else
{
// otherwise, check that they compare according to their definition
// as the inverse of less than with the operand order reversed
CHECK((j_values[i] <= j_values[j]) == !(j_values[j] < j_values[i]));
}
}
}
}
SECTION("comparison: greater than")
{
// check that two values compare greater than as expected
for (size_t i = 0; i < j_values.size(); ++i)
{
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
if (json::compares_unordered(j_values[i], j_values[j]))
{
// if two values compare unordered,
// check that the boolean comparison result is always false
CHECK_FALSE(j_values[i] > j_values[j]);
}
else
{
// otherwise, check that they compare according to their definition
// as the inverse of less than or equal which is defined as
// the inverse of less than with the operand order reversed
CHECK((j_values[i] > j_values[j]) == !(j_values[i] <= j_values[j]));
CHECK((j_values[i] > j_values[j]) == !!(j_values[j] < j_values[i]));
}
}
}
}
SECTION("comparison: greater than or equal")
{
// check that two values compare greater than or equal as expected
for (size_t i = 0; i < j_values.size(); ++i)
{
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
if (json::compares_unordered(j_values[i], j_values[j], true))
{
// if two values compare unordered,
// check that the boolean result is always false
CHECK_FALSE(j_values[i] >= j_values[j]);
}
else
{
// otherwise, check that they compare according to their definition
// as the inverse of less than
CHECK((j_values[i] >= j_values[j]) == !(j_values[i] < j_values[j]));
}
}
}
}
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
SECTION("comparison: 3-way")
{
std::vector<std::vector<std::partial_ordering>> expected =
{
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
{eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 0
{eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 1
{gt, gt, eq, lt, lt, lt, lt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 2
{gt, gt, gt, eq, gt, gt, gt, gt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 3
{gt, gt, gt, lt, eq, lt, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 4
{gt, gt, gt, lt, gt, eq, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 5
{gt, gt, gt, lt, lt, lt, eq, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 6
{gt, gt, gt, lt, gt, gt, gt, eq, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 7
{gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 8
{gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 9
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, gt, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 10
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, eq, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 11
{gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, gt, lt, lt, lt, lt, lt, lt, un, un}, // 12
{gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, lt, lt, lt, lt, lt, lt, un, un}, // 13
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, eq, lt, gt, gt, lt, lt, un, un}, // 14
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, gt, eq, gt, gt, lt, lt, un, un}, // 15
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, eq, gt, lt, lt, un, un}, // 16
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, lt, eq, lt, lt, un, un}, // 17
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, lt, un, un}, // 18
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, un, un}, // 19
{un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 20
{un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 21
};
// check expected partial_ordering against expected booleans
REQUIRE(expected.size() == expected_eq.size());
REQUIRE(expected.size() == expected_lt.size());
for (size_t i = 0; i < expected.size(); ++i)
{
REQUIRE(expected[i].size() == expected_eq[i].size());
REQUIRE(expected[i].size() == expected_lt[i].size());
for (size_t j = 0; j < expected[i].size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CHECK(std::is_eq(expected[i][j]) == expected_eq[i][j]);
CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]);
if (std::is_gt(expected[i][j]))
{
CHECK((!expected_eq[i][j] && !expected_lt[i][j]));
}
}
}
// check that two values compare according to their expected ordering
REQUIRE(expected.size() == j_values.size());
for (size_t i = 0; i < j_values.size(); ++i)
{
REQUIRE(expected[i].size() == j_values.size());
for (size_t j = 0; j < j_values.size(); ++j)
{
CAPTURE(i)
CAPTURE(j)
CHECK((j_values[i] <=> j_values[j]) == expected[i][j]); // *NOPAD*
}
}
}
#endif
}
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
SECTION("parser callback regression")
{
SECTION("filter specific element")
{
const auto* s_object = R"(
{
"foo": 2,
"bar": {
"baz": 1
}
}
)";
const auto* s_array = R"(
[1,2,[3,4,5],4,5]
)";
const json j_object = json::parse(s_object, [](int /*unused*/, json::parse_event_t /*unused*/, const json & j) noexcept
{
// filter all number(2) elements
return j != json(2);
});
CHECK (j_object == json({{"bar", {{"baz", 1}}}}));
const json j_array = json::parse(s_array, [](int /*unused*/, json::parse_event_t /*unused*/, const json & j) noexcept
{
return j != json(2);
});
CHECK (j_array == json({1, {3, 4, 5}, 4, 5}));
}
}
#endif
}
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P2468R2)")
{
// Issue #3868: operator!= was preventing compiler from synthesizing reversed
// operator== candidates under C++20's P2468R2 rewritten candidate rules.
// Verify that heterogeneous comparisons now work.
SECTION("string vs json")
{
std::string s = "string";
json j = "string";
CHECK(s == j);
CHECK(j == s);
CHECK_FALSE(s != j);
CHECK_FALSE(j != s);
}
SECTION("other heterogeneous types")
{
int i = 42;
json j = 42;
CHECK(i == j);
CHECK(j == i);
CHECK_FALSE(i != j);
CHECK_FALSE(j != i);
}
}
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
TEST_CASE("regression #5665 - scalar <= discarded and scalar >= discarded in C++20 legacy mode")
{
// Issue #5665: with a scalar on the left-hand side, <= and >= only had the
// candidate rewritten from operator<=>, which does not emulate the legacy
// discarded-value behavior. Check that scalar-on-the-left now matches the
// other three operand orders.
const json discarded(json::value_t::discarded);
const json one = 1;
CHECK(discarded <= 1);
CHECK(discarded >= 1);
CHECK(one <= discarded);
CHECK(one >= discarded);
CHECK(1 <= discarded);
CHECK(1 >= discarded);
CHECK(1.5 <= discarded);
CHECK(1.5 >= discarded);
}
#endif
#endif
namespace
{
// orders keys ascending or descending, as chosen when a map is created
template<class Key>
class directed_less
{
public:
directed_less() = default;
explicit directed_less(const bool descending) noexcept
: m_descending(descending)
{}
bool operator()(const Key& lhs, const Key& rhs) const
{
return m_descending ? rhs < lhs : lhs < rhs;
}
private:
bool m_descending = false;
};
// An object type that, like std::unordered_map, enumerates its entries in no
// fixed order - ascending or descending by key, depending on how the map was
// created - and whose operator== does not depend on that order.
// std::unordered_map itself cannot be used here: the standard does not
// require it to accept an incomplete mapped type such as basic_json, and
// libstdc++ 6 to 9 as well as the EDG front ends of icpc and nvc++ reject
// basic_json<std::unordered_map>. std::map, the default object type, works
// with all supported compilers.
template<class Key, class Value, class /*Compare*/, class Allocator>
struct unordered_object_t : std::map<Key, Value, directed_less<Key>, Allocator>
{
using base_type = std::map<Key, Value, directed_less<Key>, Allocator>;
using base_type::base_type;
friend bool operator==(const unordered_object_t& lhs, const unordered_object_t& rhs)
{
return lhs.size() == rhs.size() && std::all_of(lhs.begin(), lhs.end(), [&rhs](const std::pair<const Key, Value>& entry)
{
const auto it = rhs.find(entry.first);
return it != rhs.end() && it->second == entry.second;
});
}
friend bool operator!=(const unordered_object_t& lhs, const unordered_object_t& rhs)
{
return !(lhs == rhs);
}
};
using unordered_json = nlohmann::json::with_object_t<unordered_object_t>;
// the entries "0" to "9", enumerated in ascending or in descending order
unordered_json make_unordered_object(const bool descending)
{
unordered_json j = unordered_json::object_t(directed_less<std::string>(descending));
for (int i = 0; i < 10; ++i)
{
j[std::to_string(i)] = i;
}
return j;
}
template<typename Json>
Json nest(Json j, const std::size_t depth)
{
for (std::size_t i = 0; i < depth; ++i)
{
Json outer = Json::object();
outer["x"] = std::move(j);
j = std::move(outer);
}
return j;
}
// a std::map comparator with state: case-insensitive, unless constructed
// case-sensitive. Used to check that copying an object copies the original's
// comparator rather than default-constructing a new one (see #5649).
struct key_case_less
{
key_case_less() = default;
explicit key_case_less(const bool cs) noexcept : case_sensitive(cs) {}
bool operator()(const std::string& a, const std::string& b) const
{
if (case_sensitive)
{
return a < b;
}
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
[](unsigned char x, unsigned char y)
{
return std::tolower(x) < std::tolower(y);
});
}
bool case_sensitive = false;
};
template<class Key, class Value, class /*Compare*/, class Allocator>
using key_case_map = std::map<Key, Value, key_case_less, Allocator>;
using key_case_json = nlohmann::json::with_object_t<key_case_map>;
// the innermost value of a chain of single-element arrays
template<typename Json>
const Json& innermost(const Json& j)
{
const Json* p = &j;
while (p->is_array())
{
p = &(*p)[0];
}
return *p;
}
// orders keys case-insensitively, so "key" and "KEY" compare equivalent
// (neither less than the other) although they are not equal
struct case_insensitive_less
{
bool operator()(const std::string& a, const std::string& b) const
{
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
[](unsigned char x, unsigned char y)
{
return std::tolower(x) < std::tolower(y);
});
}
};
template<class Key, class Value, class /*Compare*/, class Allocator>
using case_insensitive_map = std::map<Key, Value, case_insensitive_less, Allocator>;
using ci_json = nlohmann::json::with_object_t<case_insensitive_map>;
} // namespace
TEST_CASE("equality of objects whose entries have no fixed order")
{
// Values nested deeper than a bound are compared without the call stack,
// entry by entry. That must agree with the object type's own operator==,
// which for unordered_object_t (as for std::unordered_map) does not
// depend on the order of the entries, and for ordered_map does.
REQUIRE(make_unordered_object(true).begin().key() == "9");
REQUIRE(make_unordered_object(false).begin().key() == "0");
for (const std::size_t depth : std::vector<std::size_t> {0, 200})
{
CAPTURE(depth)
const unordered_json descending = nest(make_unordered_object(true), depth);
const unordered_json ascending = nest(make_unordered_object(false), depth);
CHECK(descending == ascending);
CHECK_FALSE(descending != ascending);
// a copy is equal to its original
const unordered_json copy = descending; // NOLINT(performance-unnecessary-copy-initialization)
CHECK(copy == descending);
// a different value, a different key, or another entry still count
unordered_json other_value = make_unordered_object(true);
other_value["5"] = 42;
CHECK_FALSE(nest(other_value, depth) == ascending);
unordered_json other_key = make_unordered_object(true);
other_key.erase("5");
other_key["50"] = 5;
CHECK_FALSE(nest(other_key, depth) == ascending);
unordered_json more_entries = make_unordered_object(true);
more_entries["10"] = 10;
CHECK_FALSE(nest(more_entries, depth) == ascending);
CHECK_FALSE(ascending == nest(more_entries, depth));
// ordered_json compares its entries in sequence
const nlohmann::ordered_json ab = nest(nlohmann::ordered_json({{"a", 1}, {"b", 2}}), depth);
const nlohmann::ordered_json ba = nest(nlohmann::ordered_json({{"b", 2}, {"a", 1}}), depth);
CHECK_FALSE(ab == ba);
CHECK(ab != ba);
}
}
TEST_CASE("copying an object preserves its comparator's state")
{
// Past the iterative deep copy's nesting bound, an object copy used to be
// built with a default-constructed comparator instead of a copy of the
// original's. For an object type whose comparator carries state - here, a
// std::map that compares keys case-sensitively only when created that way
// - this reordered the copy's keys and could even drop entries that the
// original's comparator kept distinct (see #5649).
key_case_json object = key_case_json::object_t(key_case_less(true)); // case-sensitive
object["b"] = 1;
object["B"] = 2;
object["a"] = 3;
REQUIRE(object.dump() == R"({"B":2,"a":3,"b":1})");
for (const std::size_t depth : depths_around_recursion_limit())
{
CAPTURE(depth)
key_case_json original = object;
for (std::size_t i = 0; i < depth; ++i)
{
original = key_case_json::array({std::move(original)});
}
{
const key_case_json copy = original; // NOLINT(performance-unnecessary-copy-initialization)
CHECK(innermost(copy).size() == 3);
CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})");
CHECK(copy == original);
}
{
key_case_json copy = key_case_json::array();
copy = original;
CHECK(innermost(copy).size() == 3);
CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})");
CHECK(copy == original);
}
}
}
TEST_CASE("equality of an object whose comparator treats different keys as equivalent")
{
// https://github.com/nlohmann/json/issues/5655: past the nesting bound,
// the entries are compared without the call stack, and a key that finds
// no counterpart at the same position is looked up with find(), which
// uses the object's own comparator. A case-insensitive comparator then
// finds "KEY" for "key" and must not accept that pair as a match - the
// object type's own operator==, like std::map's, compares keys with ==.
ci_json a = ci_json::object();
a["key"] = 1;
ci_json b = ci_json::object();
b["KEY"] = 1;
// sanity check: the object type's own comparison already disagrees
CHECK_FALSE(a.get_ref<const ci_json::object_t&>() == b.get_ref<const ci_json::object_t&>());
for (const std::size_t depth : depths_around_recursion_limit())
{
CAPTURE(depth)
const ci_json x = nest(a, depth);
const ci_json y = nest(b, depth);
CHECK_FALSE(x == y);
CHECK(x != y);
}
}
TEST_CASE("containers are compared element by element")
{
// Containers nested deeper than a bound are compared without the call
// stack, by code of their own; every relation is checked both at the top
// level and below that bound.
const auto deep = [](const json & j, const std::size_t depth)
{
json result = j;
for (std::size_t i = 0; i < depth; ++i)
{
result = json::array({std::move(result)});
}
return result;
};
for (const std::size_t depth : std::vector<std::size_t> {0, 200})
{
CAPTURE(depth)
// objects with different keys
{
const json a = deep({{"a", 1}}, depth);
const json b = deep({{"b", 1}}, depth);
CHECK_FALSE(a == b);
CHECK(a != b);
CHECK(a < b);
CHECK(b > a);
CHECK_FALSE(b < a);
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
CHECK((a <=> b) == std::partial_ordering::less); // *NOPAD*
CHECK((b <=> a) == std::partial_ordering::greater); // *NOPAD*
CHECK((a <=> a) == std::partial_ordering::equivalent); // *NOPAD*
#endif
}
// a container that is a prefix of the other one
{
// the one that runs out of elements first is the smaller one
const json shorter = deep({1}, depth);
const json longer = deep({1, 2}, depth);
CHECK(shorter < longer);
CHECK(longer > shorter);
CHECK_FALSE(longer < shorter);
CHECK_FALSE(shorter == longer);
const json smaller_object = deep({{"a", 1}}, depth);
const json larger_object = deep({{"a", 1}, {"b", 2}}, depth);
CHECK(smaller_object < larger_object);
CHECK(larger_object > smaller_object);
CHECK_FALSE(smaller_object == larger_object);
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
CHECK((shorter <=> longer) == std::partial_ordering::less); // *NOPAD*
CHECK((longer <=> shorter) == std::partial_ordering::greater); // *NOPAD*
#endif
}
// elements that cannot be ordered
{
const double nan = std::numeric_limits<double>::quiet_NaN();
const json lhs = deep({nan, 1}, depth);
const json rhs = deep({nan, 2}, depth);
CHECK_FALSE(lhs == lhs);
CHECK_FALSE(rhs < lhs);
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
// operator<=> stops there, as std::lexicographical_compare_three_way
// does, and operator< is derived from it
CHECK((lhs <=> rhs) == std::partial_ordering::unordered); // *NOPAD*
CHECK_FALSE(lhs < rhs);
#else
// operator< skips a pair of elements that cannot be ordered, as
// std::lexicographical_compare does, and the next pair decides
CHECK(lhs < rhs);
#endif
}
}
}
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
TEST_CASE("operator<=> of binary values with a different subtype does not depend on nesting depth")
{
// #5654: std::vector<std::uint8_t>::operator<=>, which the binary type's
// own operator<=> uses, ignores the subtype that operator== checks. So a
// pair of binary values with the same bytes but a different subtype is
// unequal, yet <=>-equivalent - the same inconsistency between == and <=>
// that a NaN has. Within the nesting bound, an array compares itself
// with std::vector's own operator<=>, which treats an equivalent pair as
// undecided and lets the next element decide, same as
// std::lexicographical_compare_three_way does. Past the bound,
// compare_iteratively<true>() takes over and must classify the pair the
// same way, or the result of operator<=> - and of <, which C++20 derives
// from it - depends on how deeply the values are nested.
const json a = json::array({json::binary({1}, 1), 1});
const json b = json::array({json::binary({1}, 2), 2});
// the root inconsistency: unequal, yet <=>-equivalent
CHECK_FALSE(a[0] == b[0]);
CHECK((a[0] <=> b[0]) == std::partial_ordering::equivalent); // *NOPAD*
const auto deep = [](const json & j, const std::size_t depth)
{
json result = j;
for (std::size_t i = 0; i < depth; ++i)
{
result = json::array({std::move(result)});
}
return result;
};
// one level less than the recursion depth limit (127 by default) stays within it;
// the limit itself and more (128 and 200) do not, and must still agree with the
// levels that do
for (const std::size_t depth : depths_around_recursion_limit())
{
CAPTURE(depth)
const json x = deep(a, depth);
const json y = deep(b, depth);
CHECK((x <=> y) == std::partial_ordering::less); // *NOPAD*
CHECK((y <=> x) == std::partial_ordering::greater); // *NOPAD*
CHECK(x < y);
CHECK(y > x);
CHECK_FALSE(y < x);
}
}
#endif
TEST_CASE("comparison of scalars (inline fast path)")
{
const json::number_integer_t neg = -1;
const json::number_integer_t pos = 5;
const json::number_unsigned_t big = (std::numeric_limits<json::number_unsigned_t>::max)();
SECTION("same type")
{
CHECK(json(pos) == json(pos));
CHECK(json(pos) != json(neg));
CHECK(json(neg) < json(pos));
CHECK_FALSE(json(pos) < json(neg));
CHECK_FALSE(json(pos) < json(pos));
CHECK(json(pos) <= json(pos));
CHECK(json(1u) == json(1u));
CHECK(json(1u) < json(2u));
CHECK_FALSE(json(2u) < json(1u));
CHECK(json(1.5) == json(1.5));
CHECK(json(1.5) < json(2.5));
CHECK_FALSE(json(2.5) < json(1.5));
CHECK(json(-0.0) == json(0.0));
}
SECTION("mixed number types")
{
// negative signed vs unsigned must not wrap around
CHECK(json(neg) < json(1u));
CHECK(json(1u) > json(neg));
CHECK(json(neg) != json(big));
CHECK(json(neg) < json(big));
CHECK_FALSE(json(big) < json(neg));
CHECK(json(pos) == json(5u));
CHECK(json(5u) == json(pos));
CHECK(json(pos) < json(6u));
CHECK(json(pos) == json(5.0));
CHECK(json(5.0) == json(pos));
CHECK(json(5u) == json(5.0));
CHECK(json(neg) < json(0.5));
CHECK(json(0.5) < json(1u));
}
SECTION("boolean and null")
{
CHECK(json(true) == json(true));
CHECK(json(true) != json(false));
CHECK(json(false) < json(true));
CHECK_FALSE(json(true) < json(false));
CHECK_FALSE(json(true) < json(true));
CHECK(json(nullptr) == json(nullptr));
CHECK_FALSE(json(nullptr) < json(nullptr));
CHECK(json(nullptr) <= json(nullptr));
CHECK(json(nullptr) < json(false));
CHECK(json(nullptr) != json(false));
CHECK(json(nullptr) != json(0));
}
SECTION("string vs number")
{
CHECK(json("5") != json(5));
CHECK(json(5) != json("5"));
CHECK(json(5) < json("5")); // number < string by type order
CHECK_FALSE(json("5") < json(5));
CHECK(json(1.5) < json(""));
CHECK(json("a") == json("a"));
CHECK(json("a") < json("b"));
}
SECTION("NaN")
{
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
const json one = 1.0;
CHECK_FALSE(nan == nan);
CHECK(nan != nan);
CHECK_FALSE(nan < nan);
CHECK_FALSE(nan <= nan);
CHECK_FALSE(nan >= nan);
CHECK_FALSE(nan > nan);
CHECK_FALSE(nan == one);
CHECK_FALSE(one == nan);
CHECK_FALSE(nan < one);
CHECK_FALSE(one < nan);
CHECK_FALSE(nan <= one);
CHECK_FALSE(one <= nan);
CHECK_FALSE(nan >= one);
CHECK_FALSE(one >= nan);
// NaN against other number types
CHECK_FALSE(nan == json(1));
CHECK_FALSE(json(1) == nan);
CHECK_FALSE(nan < json(1u));
CHECK_FALSE(json(1u) < nan);
#if JSON_HAS_THREE_WAY_COMPARISON
CHECK((nan <=> nan) == std::partial_ordering::unordered); // *NOPAD*
CHECK((nan <=> one) == std::partial_ordering::unordered); // *NOPAD*
CHECK((one <=> nan) == std::partial_ordering::unordered); // *NOPAD*
CHECK((one <=> json(2.0)) == std::partial_ordering::less); // *NOPAD*
CHECK((json(2.0) <=> one) == std::partial_ordering::greater); // *NOPAD*
CHECK((one <=> one) == std::partial_ordering::equivalent); // *NOPAD*
CHECK((json(neg) <=> json(1u)) == std::partial_ordering::less); // *NOPAD*
CHECK((json(nullptr) <=> json(nullptr)) == std::partial_ordering::equivalent); // *NOPAD*
CHECK((json(true) <=> json(false)) == std::partial_ordering::greater); // *NOPAD*
#endif
}
SECTION("discarded")
{
const json d(json::value_t::discarded);
CHECK_FALSE(d == d);
CHECK_FALSE(d < d);
CHECK_FALSE(d == json(1));
CHECK_FALSE(json(1) == d);
CHECK_FALSE(d < json(1));
CHECK_FALSE(json(1) < d);
CHECK_FALSE(d == json(nullptr));
CHECK_FALSE(json(nullptr) == d);
#if JSON_HAS_THREE_WAY_COMPARISON
CHECK((d <=> d) == std::partial_ordering::unordered); // *NOPAD*
CHECK((d <=> json(1)) == std::partial_ordering::unordered); // *NOPAD*
CHECK((json(1) <=> d) == std::partial_ordering::unordered); // *NOPAD*
#endif
}
SECTION("scalar overloads")
{
CHECK(json(5) == 5);
CHECK(5 == json(5));
CHECK(json(5) < 6);
CHECK(4 < json(5));
CHECK(json(1.5) == 1.5);
CHECK(json(true) == true);
CHECK(json(5) != 6);
CHECK(json(1u) == 1.0);
}
SECTION("structured values still compare")
{
CHECK(json({1, 2}) == json({1, 2}));
CHECK(json({1, 2}) < json({1, 3}));
CHECK(json({{"a", 1}}) == json({{"a", 1}}));
CHECK(json::binary({1, 2}) == json::binary({1, 2}));
CHECK(json(1) < json::array());
CHECK(json::object() < json::array());
}
}