From 4f69be80d6f27b0d6d7069514d94ea332e25f113 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 18:20:20 +0200 Subject: [PATCH 1/6] Clarify wording in diff documentation (#5759) Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/diff.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/diff.md b/docs/mkdocs/docs/api/basic_json/diff.md index fb6683ec8..ae22454dd 100644 --- a/docs/mkdocs/docs/api/basic_json/diff.md +++ b/docs/mkdocs/docs/api/basic_json/diff.md @@ -8,7 +8,7 @@ static basic_json diff(const basic_json& source, Creates a [JSON Patch](http://jsonpatch.com) so that value `source` can be changed into the value `target` by calling [`patch`](patch.md) function. -For two JSON values `source` and `target`, the following code yields always `#!cpp true`: +For two JSON values `source` and `target`, the following code always yields `#!cpp true`: ```cpp source.patch(diff(source, target)) == target; ``` @@ -27,7 +27,7 @@ a JSON patch to convert the `source` to `target` ## Exception safety -Strong guarantee: if an exception is thrown, there are no changes in the JSON value. +Strong guarantee: `source` and `target` are never modified. ## Complexity From c5a7a4b46d91231e3ad3bf5572f0cc165f356379 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 22:11:34 +0200 Subject: [PATCH 2/6] Add deprecated from_bon8/from_bjdata(ptr, len) overloads (#5688) from_bon8(ptr, len) and from_bjdata(ptr, len) had no overload for a pointer and a length, unlike from_cbor/from_msgpack/from_ubjson/ from_bson. The call instead bound to from_*(InputType&&, bool strict), which read ptr as a NUL-terminated C string via strlen and silently converted len to the strict flag. Data containing a 0x00 byte was cut off there; data without one was read past the end of the buffer. Add a deprecated (ptr, len, strict, allow_exceptions) overload for each function that forwards to (ptr, ptr + len, ...), matching the existing deprecated overloads of the other four binary readers. Since neither function ever had this overload, the deprecation is declared as of version 3.13.0, the next unreleased version, rather than the version each function was originally added in. Fixes #5648. Signed-off-by: Niels Lohmann --- .../mkdocs/docs/api/basic_json/from_bjdata.md | 9 ++++++ docs/mkdocs/docs/api/basic_json/from_bon8.md | 9 ++++++ include/nlohmann/json.hpp | 20 +++++++++++++ single_include/nlohmann/json.hpp | 20 +++++++++++++ tests/src/unit-bjdata.cpp | 28 +++++++++++++++++++ tests/src/unit-bon8.cpp | 28 +++++++++++++++++++ 6 files changed, 114 insertions(+) diff --git a/docs/mkdocs/docs/api/basic_json/from_bjdata.md b/docs/mkdocs/docs/api/basic_json/from_bjdata.md index a45e00ad5..f19f9e4a9 100644 --- a/docs/mkdocs/docs/api/basic_json/from_bjdata.md +++ b/docs/mkdocs/docs/api/basic_json/from_bjdata.md @@ -120,3 +120,12 @@ Linear in the size of the input. - Extended container support (1) to include types with lvalue-only ADL `begin`/`end` (matching `std::begin`/`std::end` semantics) in version 3.13.0. - Extended overload (2) to accept heterogeneous iterator+sentinel pairs (C++20 ranges support) in version 3.13.0. - Added `error_handler` parameter in version 3.13.0. + +!!! warning "Deprecation" + + - Overload (2) replaces calls to `from_bjdata` with a pointer and a length as first two parameters, which has been + deprecated in version 3.13.0. This overload will be removed in version 4.0.0. Please replace all calls like + `#!cpp from_bjdata(ptr, len, ...);` with `#!cpp from_bjdata(ptr, ptr+len, ...);`. + + You should be warned by your compiler with a `-Wdeprecated-declarations` warning if you are using a deprecated + function. diff --git a/docs/mkdocs/docs/api/basic_json/from_bon8.md b/docs/mkdocs/docs/api/basic_json/from_bon8.md index 2f9d1e749..cac6b31eb 100644 --- a/docs/mkdocs/docs/api/basic_json/from_bon8.md +++ b/docs/mkdocs/docs/api/basic_json/from_bon8.md @@ -106,3 +106,12 @@ Linear in the size of the input. ## Version history - Added in version 3.13.0. + +!!! warning "Deprecation" + + - Overload (2) replaces calls to `from_bon8` with a pointer and a length as first two parameters, which has been + deprecated in version 3.13.0. This overload will be removed in version 4.0.0. Please replace all calls like + `#!cpp from_bon8(ptr, len, ...);` with `#!cpp from_bon8(ptr, ptr+len, ...);`. + + You should be warned by your compiler with a `-Wdeprecated-declarations` warning if you are using a deprecated + function. diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 3f3645524..ca9682bd5 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -5956,6 +5956,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return from_binary_impl(detail::input_adapter(std::move(first), std::move(last)), input_format_t::bjdata, strict, allow_exceptions, error_handler); } + template + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.13.0, from_bjdata(ptr, ptr + len)) + static basic_json from_bjdata(const T* ptr, std::size_t len, + const bool strict = true, + const bool allow_exceptions = true) + { + return from_bjdata(ptr, ptr + len, strict, allow_exceptions); + } + /// @brief create a JSON value from an input in BON8 format /// @sa https://json.nlohmann.me/api/basic_json/from_bon8/ template @@ -5979,6 +5989,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return from_binary_impl(detail::input_adapter(std::move(first), std::move(last)), input_format_t::bon8, strict, allow_exceptions); } + template + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.13.0, from_bon8(ptr, ptr + len)) + static basic_json from_bon8(const T* ptr, std::size_t len, + const bool strict = true, + const bool allow_exceptions = true) + { + return from_bon8(ptr, ptr + len, strict, allow_exceptions); + } + /// @brief create a JSON value from an input in BSON format /// @sa https://json.nlohmann.me/api/basic_json/from_bson/ template diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b5e7417e6..ef6f3d26a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -33078,6 +33078,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return from_binary_impl(detail::input_adapter(std::move(first), std::move(last)), input_format_t::bjdata, strict, allow_exceptions, error_handler); } + template + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.13.0, from_bjdata(ptr, ptr + len)) + static basic_json from_bjdata(const T* ptr, std::size_t len, + const bool strict = true, + const bool allow_exceptions = true) + { + return from_bjdata(ptr, ptr + len, strict, allow_exceptions); + } + /// @brief create a JSON value from an input in BON8 format /// @sa https://json.nlohmann.me/api/basic_json/from_bon8/ template @@ -33101,6 +33111,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return from_binary_impl(detail::input_adapter(std::move(first), std::move(last)), input_format_t::bon8, strict, allow_exceptions); } + template + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.13.0, from_bon8(ptr, ptr + len)) + static basic_json from_bon8(const T* ptr, std::size_t len, + const bool strict = true, + const bool allow_exceptions = true) + { + return from_bon8(ptr, ptr + len, strict, allow_exceptions); + } + /// @brief create a JSON value from an input in BSON format /// @sa https://json.nlohmann.me/api/basic_json/from_bson/ template diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 0098541cf..06646c1d0 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -4490,3 +4490,31 @@ TEST_CASE("BJData roundtrips" * doctest::skip()) } } } + +TEST_CASE("issue #5648 - from_bjdata(ptr, len) must read len bytes, not treat ptr as a C string") +{ + // to_bjdata() encodes the integer 0 as the two bytes 'i' 0x00 (a BJData + // type marker followed by the value byte 0x00), so the packed data + // below contains a 0x00 byte before its end. + const json j = {{"a", 0}}; + const std::vector packed = json::to_bjdata(j); + bool contains_nul = false; + for (const auto byte : packed) + { + contains_nul |= (byte == 0x00); + } + REQUIRE(contains_nul); + + // before the fix, from_bjdata had no (ptr, len) overload, so this call + // bound to from_bjdata(InputType&&, bool strict) instead: ptr was read + // as a NUL-terminated C string (stopping at the embedded 0x00 byte), and + // len was silently converted to the strict flag. The deprecated + // overload added for this issue forwards to from_bjdata(ptr, ptr + len, + // ...) instead, like from_ubjson's deprecated (ptr, len) overload does. + json result; + CHECK_NOTHROW(result = json::from_bjdata(packed.data(), packed.size())); + CHECK(result == j); + + // len must not collapse into the strict flag either + CHECK(json::from_bjdata(packed.data(), packed.size(), false) == j); +} diff --git a/tests/src/unit-bon8.cpp b/tests/src/unit-bon8.cpp index 491189867..9a9116e12 100644 --- a/tests/src/unit-bon8.cpp +++ b/tests/src/unit-bon8.cpp @@ -1014,6 +1014,34 @@ TEST_CASE("BON8 roundtrips" * doctest::skip()) } } +TEST_CASE("issue #5648 - from_bon8(ptr, len) must read len bytes, not treat ptr as a C string") +{ + // to_bon8() encodes the integer 40 as the two bytes 0xC2 0x00 (a BON8 + // lead byte followed by a continuation byte of 0x00), so the packed data + // below contains a 0x00 byte before its end. + const json j = {{"a", 40}, {"b", "x"}}; + const std::vector packed = json::to_bon8(j); + bool contains_nul = false; + for (const auto byte : packed) + { + contains_nul |= (byte == 0x00); + } + REQUIRE(contains_nul); + + // before the fix, from_bon8 had no (ptr, len) overload, so this call + // bound to from_bon8(InputType&&, bool strict) instead: ptr was read as + // a NUL-terminated C string (stopping at the embedded 0x00 byte), and + // len was silently converted to the strict flag. The deprecated + // overload added for this issue forwards to from_bon8(ptr, ptr + len, + // ...) instead, like from_cbor's deprecated (ptr, len) overload does. + json result; + CHECK_NOTHROW(result = json::from_bon8(packed.data(), packed.size())); + CHECK(result == j); + + // len must not collapse into the strict flag either + CHECK(json::from_bon8(packed.data(), packed.size(), false) == j); +} + #ifdef JSON_HAS_CPP_17 TEST_CASE("BON8 with std::byte") { From 23d3b373e1a41db5adde1081b409e66de1d65030 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 22:11:34 +0200 Subject: [PATCH 3/6] Add JSON_DELETE_DEPRECATED_FUNCTIONS to delete the deprecated functions (#5755) * Add JSON_DELETE_DEPRECATED_FUNCTIONS to delete the deprecated functions Defining JSON_DELETE_DEPRECATED_FUNCTIONS to 1 (or the CMake option JSON_DeleteDeprecatedFunctions) declares every deprecated function as deleted instead of deprecated, so that code that is not ready for 4.0.0 no longer compiles. A deleted function still takes part in overload resolution, so from_*(ptr, len) cannot silently bind len to the strict parameter of from_*(InputType&&, bool); the roadmap now plans to keep these overloads deleted in 4.0.0 instead of removing them. The legacy discarded-value comparison is left to its own macro. Also update the 4.0 roadmap: add JSON_DISABLE_TUPLE_REFERENCE_CONVERSION and JSON_DELETE_DEPRECATED_FUNCTIONS to the macro table, add the from_bjdata/from_bon8 (ptr, len) overloads to the deprecated functions, document the macro in the migration guide, and fix the docs style check findings (example titles, missing docset entry for JSON_STRICT_BINARY_UTF8). Signed-off-by: Niels Lohmann * Declare each deprecated function once and guard only its body Instead of repeating every deprecated declaration in an #if JSON_DELETE_DEPRECATED_FUNCTIONS branch, keep one declaration (with its deprecation attribute) and switch only between "= delete;" and the function body. Suggested by @gregmarr in the review. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .github/workflows/ubuntu.yml | 2 +- CMakeLists.txt | 6 + cmake/ci.cmake | 16 +- docs/docset/docSet.sql | 1 + docs/mkdocs/docs/api/macros/index.md | 5 + .../json_delete_deprecated_functions.md | 96 ++++++++ ...json_disable_tuple_reference_conversion.md | 1 + .../macros/json_use_implicit_conversions.md | 2 +- docs/mkdocs/docs/community/roadmap.md | 14 +- docs/mkdocs/docs/features/macros.md | 14 +- docs/mkdocs/docs/integration/cmake.md | 6 + .../docs/integration/migration_guide.md | 13 +- docs/mkdocs/mkdocs.yml | 1 + include/nlohmann/detail/json_pointer.hpp | 24 ++ include/nlohmann/detail/macro_scope.hpp | 4 + include/nlohmann/detail/macro_unscope.hpp | 1 + include/nlohmann/json.hpp | 96 ++++++++ single_include/nlohmann/json.hpp | 125 ++++++++++ tests/src/unit-bjdata.cpp | 10 + tests/src/unit-bon8.cpp | 10 + tests/src/unit-bson.cpp | 12 + tests/src/unit-cbor.cpp | 12 + .../src/unit-delete_deprecated_functions.cpp | 214 ++++++++++++++++++ tests/src/unit-deserialization.cpp | 16 +- tests/src/unit-items.cpp | 10 + tests/src/unit-json_pointer.cpp | 23 +- tests/src/unit-msgpack.cpp | 12 + tests/src/unit-regression2.cpp | 2 +- tests/src/unit-serialization.cpp | 10 + tests/src/unit-ubjson.cpp | 12 + 30 files changed, 755 insertions(+), 15 deletions(-) create mode 100644 docs/mkdocs/docs/api/macros/json_delete_deprecated_functions.md create mode 100644 tests/src/unit-delete_deprecated_functions.cpp diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 1e4a72870..354cf2391 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -107,7 +107,7 @@ jobs: container: ubuntu:24.04 strategy: matrix: - target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_no_thread_local] + target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local] steps: - name: Install build-essential run: apt-get update ; apt-get install -y build-essential unzip wget git diff --git a/CMakeLists.txt b/CMakeLists.txt index b5092ae49..a2501a92d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,7 @@ option(JSON_MultipleHeaders "Use non-amalgamated version of the l option(JSON_SystemInclude "Include as system headers (skip for clang-tidy)." OFF) option(JSON_StrictNulHandling "Build with strict NUL-byte handling enabled." OFF) option(JSON_StrictBinaryUTF8 "Build with UTF-8 checks in the CBOR, UBJSON, BJData, and BSON writers enabled." OFF) +option(JSON_DeleteDeprecatedFunctions "Delete the deprecated functions instead of only deprecating them." OFF) if (JSON_CI) include(ci) @@ -123,6 +124,10 @@ if (JSON_StrictBinaryUTF8) message(STATUS "Strict UTF-8 checks in binary writers enabled (JSON_STRICT_BINARY_UTF8=1)") endif() +if (JSON_DeleteDeprecatedFunctions) + message(STATUS "Deprecated functions are deleted (JSON_DELETE_DEPRECATED_FUNCTIONS=1)") +endif() + if (JSON_Diagnostic_Positions) message(STATUS "Diagnostic positions enabled (JSON_DIAGNOSTIC_POSITIONS=1)") endif() @@ -159,6 +164,7 @@ target_compile_definitions( $<$:JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1> $<$:JSON_STRICT_NUL_HANDLING=1> $<$:JSON_STRICT_BINARY_UTF8=1> + $<$:JSON_DELETE_DEPRECATED_FUNCTIONS=1> ) target_include_directories( diff --git a/cmake/ci.cmake b/cmake/ci.cmake index d0af68786..b9d246bbf 100644 --- a/cmake/ci.cmake +++ b/cmake/ci.cmake @@ -249,6 +249,20 @@ add_custom_target(ci_test_strict_nul_handling COMMENT "Compile and test with strict NUL-byte handling enabled" ) +############################################################################### +# Delete the deprecated functions. +############################################################################### + +add_custom_target(ci_test_delete_deprecated_functions + COMMAND ${CMAKE_COMMAND} + -DCMAKE_BUILD_TYPE=Debug -GNinja + -DJSON_BuildTests=ON -DJSON_FastTests=ON -DJSON_DeleteDeprecatedFunctions=ON + -S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_delete_deprecated_functions + COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_delete_deprecated_functions + COMMAND cd ${PROJECT_BINARY_DIR}/build_delete_deprecated_functions && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure + COMMENT "Compile and test with the deprecated functions deleted" +) + ############################################################################### # Disable global UDLs. ############################################################################### @@ -705,7 +719,7 @@ ci_get_cmake(4.0.0 CMAKE_4_0_0_BINARY) # the tests require CMake 3.13 or later, so they are excluded for CMake 3.5.0 set(JSON_CMAKE_FLAGS_3_5_0 JSON_Diagnostics JSON_Diagnostic_Positions JSON_GlobalUDLs JSON_ImplicitConversions JSON_DisableEnumSerialization JSON_LegacyDiscardedValueComparison JSON_Install JSON_MultipleHeaders JSON_SystemInclude JSON_Valgrind - JSON_StrictNulHandling JSON_StrictBinaryUTF8) + JSON_StrictNulHandling JSON_StrictBinaryUTF8 JSON_DeleteDeprecatedFunctions) set(JSON_CMAKE_FLAGS_3_31_6 JSON_BuildTests ${JSON_CMAKE_FLAGS_3_5_0}) set(JSON_CMAKE_FLAGS_4_0_0 JSON_BuildTests ${JSON_CMAKE_FLAGS_3_5_0}) diff --git a/docs/docset/docSet.sql b/docs/docset/docSet.sql index 801ade93a..9fc5f7e3d 100644 --- a/docs/docset/docSet.sql +++ b/docs/docset/docSet.sql @@ -219,6 +219,7 @@ INSERT INTO searchIndex(name, type, path) VALUES ('Supported Macros', 'Guide', ' INSERT INTO searchIndex(name, type, path) VALUES ('JSON_ASSERT', 'Macro', 'api/macros/json_assert/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('JSON_BRACE_INIT_COPY_SEMANTICS', 'Macro', 'api/macros/json_brace_init_copy_semantics/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('JSON_CATCH_USER', 'Macro', 'api/macros/json_throw_user/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('JSON_DELETE_DEPRECATED_FUNCTIONS', 'Macro', 'api/macros/json_delete_deprecated_functions/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('JSON_DIAGNOSTICS', 'Macro', 'api/macros/json_diagnostics/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('JSON_DIAGNOSTIC_POSITIONS', 'Macro', 'api/macros/json_diagnostic_positions/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('JSON_DISABLE_ENUM_SERIALIZATION', 'Macro', 'api/macros/json_disable_enum_serialization/index.html'); diff --git a/docs/mkdocs/docs/api/macros/index.md b/docs/mkdocs/docs/api/macros/index.md index ffeb9639c..eb2807b69 100644 --- a/docs/mkdocs/docs/api/macros/index.md +++ b/docs/mkdocs/docs/api/macros/index.md @@ -61,6 +61,11 @@ header. See also the [macro overview page](../../features/macros.md). - [**JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS**](json_use_objects_for_enum_keyed_maps.md) - opt in to storing maps with enum keys as objects +## Deprecated functions + +- [**JSON_DELETE_DEPRECATED_FUNCTIONS**](json_delete_deprecated_functions.md) - opt in to deleting the deprecated + functions ahead of their removal in version 4.0.0 + ## Comparison behavior - [**JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON**](json_use_legacy_discarded_value_comparison.md) - diff --git a/docs/mkdocs/docs/api/macros/json_delete_deprecated_functions.md b/docs/mkdocs/docs/api/macros/json_delete_deprecated_functions.md new file mode 100644 index 000000000..20f0ead0c --- /dev/null +++ b/docs/mkdocs/docs/api/macros/json_delete_deprecated_functions.md @@ -0,0 +1,96 @@ +# JSON_DELETE_DEPRECATED_FUNCTIONS + +```cpp +#define JSON_DELETE_DEPRECATED_FUNCTIONS /* value */ +``` + +When defined to `1`, all [deprecated functions](../../community/roadmap.md#removal-of-deprecated-functions) of the +library are declared as deleted (`= delete`) instead of only being marked as deprecated. Code that still calls one of +them no longer compiles. This way, you can find all calls that need to be replaced before version 4.0.0 removes these +functions; the [migration guide](../../integration/migration_guide.md#replace-deprecated-functions) describes how. + +A deleted function, unlike a removed one, still takes part in overload resolution. A call that would select it +therefore fails to compile instead of silently selecting another overload. This matters for the deprecated +`from_*(ptr, len)` overloads of [`from_cbor`](../basic_json/from_cbor.md), [`from_msgpack`](../basic_json/from_msgpack.md), +[`from_ubjson`](../basic_json/from_ubjson.md), [`from_bjdata`](../basic_json/from_bjdata.md), +[`from_bon8`](../basic_json/from_bon8.md), and [`from_bson`](../basic_json/from_bson.md): without them, a call like +`from_cbor(ptr, len)` would compile, read `ptr` as a NUL-terminated string, and convert `len` to the `strict` parameter. + +The macro does not affect the deprecated legacy comparison of discarded values, which is controlled by +[`JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON`](json_use_legacy_discarded_value_comparison.md). + +## Default definition + +The default value is `0` (disabled, the deprecated functions can still be called, and the compiler warns about it). + +```cpp +#define JSON_DELETE_DEPRECATED_FUNCTIONS 0 +``` + +## Notes + +!!! info "CMake option" + + The macro can also be set with the CMake option + [`JSON_DeleteDeprecatedFunctions`](../../integration/cmake.md#json_deletedeprecatedfunctions) (`OFF` by default). + +!!! warning "Opt-in only" + + This macro must be defined **before** including ``. Defining it after the include has no + effect. Define it for the whole project to avoid different declarations of the same class in different + translation units. + +!!! note "ABI compatibility" + + The macro only turns calls that compile into calls that do not; it does not change the layout or the behavior of + any type. Its value is therefore not encoded in the [namespace](../../features/namespace.md). + +## Examples + +??? example "Example: default behavior (macro not defined)" + + Without the macro, the deprecated overload is called, and the compiler warns about it: + + ```cpp + #include + + using json = nlohmann::json; + + int main() + { + const std::vector v = {0x82, 0x01, 0x02}; + auto j = json::from_cbor(v.data(), v.size()); + // warning: 'from_cbor' is deprecated: Since 3.8.0; use from_cbor(ptr, ptr + len) + } + ``` + +??? example "Example: deleted deprecated functions (macro defined to 1)" + + With the macro, the call does not compile: + + ```cpp + #define JSON_DELETE_DEPRECATED_FUNCTIONS 1 + #include + + using json = nlohmann::json; + + int main() + { + const std::vector v = {0x82, 0x01, 0x02}; + auto j = json::from_cbor(v.data(), v.size()); + // error: call to deleted function 'from_cbor' + } + ``` + +## See also + +- [Roadmap: removal of deprecated functions](../../community/roadmap.md#removal-of-deprecated-functions) - the + deprecated functions and the version they were deprecated in +- [Migration guide: replace deprecated functions](../../integration/migration_guide.md#replace-deprecated-functions) - + how to replace each deprecated function + +## Version history + +- Added in version 3.13.0. +- Planned to be removed in version 4.0.0, which removes the deprecated functions. The deprecated `from_*(ptr, len)` + overloads stay deleted in version 4.0.0. diff --git a/docs/mkdocs/docs/api/macros/json_disable_tuple_reference_conversion.md b/docs/mkdocs/docs/api/macros/json_disable_tuple_reference_conversion.md index c69240726..0a2216b57 100644 --- a/docs/mkdocs/docs/api/macros/json_disable_tuple_reference_conversion.md +++ b/docs/mkdocs/docs/api/macros/json_disable_tuple_reference_conversion.md @@ -112,3 +112,4 @@ The default value is `0` (disabled — existing behavior is preserved). ## Version history - Added in version 3.13.0. +- Planned to become the default (with the macro removed) in version 4.0.0. diff --git a/docs/mkdocs/docs/api/macros/json_use_implicit_conversions.md b/docs/mkdocs/docs/api/macros/json_use_implicit_conversions.md index 113344412..675a8acee 100644 --- a/docs/mkdocs/docs/api/macros/json_use_implicit_conversions.md +++ b/docs/mkdocs/docs/api/macros/json_use_implicit_conversions.md @@ -44,7 +44,7 @@ By default, implicit conversions are enabled. ## Examples -??? example "Example: implicit conversion" +??? example "Example: implicit and explicit conversions" This is an example for an implicit conversion: diff --git a/docs/mkdocs/docs/community/roadmap.md b/docs/mkdocs/docs/community/roadmap.md index 48977b295..36d5d7e7f 100644 --- a/docs/mkdocs/docs/community/roadmap.md +++ b/docs/mkdocs/docs/community/roadmap.md @@ -64,6 +64,8 @@ The following macros guard changes that are planned to become the default in ver | [`JSON_PRECISE_STREAM_POSITION`](../api/macros/json_precise_stream_position.md) | `0` | `1`: reading from a stream does not consume the character after a number | – | 3.13.0 | | [`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md) | `0` | `1`: a NUL byte in the input is a parse error instead of the end of input | [`JSON_StrictNulHandling`](../integration/cmake.md#json_strictnulhandling) | 3.13.0 | | [`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md) | `0` | `1`: `to_cbor`, `to_ubjson`, `to_bjdata`, and `to_bson` throw for strings that are not valid UTF-8 by default | [`JSON_StrictBinaryUTF8`](../integration/cmake.md#json_strictbinaryutf8) | 3.13.0 | +| [`JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md) | `0` | `1`: a `basic_json` value can no longer be created from a one-element tuple of a reference to it, such as `std::forward_as_tuple(j)`| [`JSON_DisableTupleReferenceConversion`](../integration/cmake.md#json_disabletuplereferenceconversion) | 3.13.0 | +| [`JSON_DELETE_DEPRECATED_FUNCTIONS`](../api/macros/json_delete_deprecated_functions.md) | `0` | removed: the deprecated functions are removed (see below); the `from_*(ptr, len)` overloads stay deleted | [`JSON_DeleteDeprecatedFunctions`](../integration/cmake.md#json_deletedeprecatedfunctions) | 3.13.0 | For example, the following makes a 3.x release behave like version 4.0 with respect to these changes: @@ -75,6 +77,8 @@ For example, the following makes a 3.x release behave like version 4.0 with resp #define JSON_PRECISE_STREAM_POSITION 1 #define JSON_STRICT_NUL_HANDLING 1 #define JSON_STRICT_BINARY_UTF8 1 +#define JSON_DISABLE_TUPLE_REFERENCE_CONVERSION 1 +#define JSON_DELETE_DEPRECATED_FUNCTIONS 1 #include ``` @@ -84,8 +88,13 @@ way to achieve this. ### Removal of deprecated functions Version 4.0 will remove all deprecated functions. Compiling with deprecation warnings enabled shows which of them your -code still uses. The [migration guide](../integration/migration_guide.md#replace-deprecated-functions) shows how to -replace each of them. +code still uses. Defining [`JSON_DELETE_DEPRECATED_FUNCTIONS`](../api/macros/json_delete_deprecated_functions.md) to +`1` turns these warnings into errors, as the deprecated functions are then deleted. The +[migration guide](../integration/migration_guide.md#replace-deprecated-functions) shows how to replace each of them. + +The `from_*` overloads taking a pointer and a length are not removed in version 4.0, but stay deleted. Without them, a +call like `from_cbor(ptr, len)` would still compile: it would read `ptr` as a NUL-terminated string and convert `len` +to the `strict` parameter. | Deprecated | Since | Migration | |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|----------------------------------------------------------------------------------| @@ -97,6 +106,7 @@ replace each of them. | [`json_pointer::operator string_t`](../api/json_pointer/operator_string_t.md) | 3.11.0 | [JSON Pointers](../integration/migration_guide.md#json-pointers) | | [`json_pointer`](../api/json_pointer/index.md) with a `basic_json` type as template argument, and the overloads of `value`, `contains`, `operator[]`, and `at` accepting such a pointer | 3.11.0 | [JSON Pointers](../integration/migration_guide.md#json-pointers) | | Comparing a [`json_pointer`](../api/json_pointer/index.md) with a string via [`operator==`](../api/json_pointer/operator_eq.md) or [`operator!=`](../api/json_pointer/operator_ne.md) | 3.11.2 | [JSON Pointers](../integration/migration_guide.md#json-pointers) | +| [`from_bjdata`](../api/basic_json/from_bjdata.md) and [`from_bon8`](../api/basic_json/from_bon8.md) with `(ptr, len)` | 3.13.0 | [Parsing](../integration/migration_guide.md#parsing) | The deprecated legacy comparison of discarded values is controlled by a macro and therefore listed in the table above. diff --git a/docs/mkdocs/docs/features/macros.md b/docs/mkdocs/docs/features/macros.md index 4fd05c12c..ef41ce0d5 100644 --- a/docs/mkdocs/docs/features/macros.md +++ b/docs/mkdocs/docs/features/macros.md @@ -23,6 +23,17 @@ This macro overrides [`#!cpp catch`](https://en.cppreference.com/w/cpp/language/ See [full documentation of `JSON_CATCH_USER(exception)`](../api/macros/json_throw_user.md). +## `JSON_DELETE_DEPRECATED_FUNCTIONS` + +When defined to `1`, all deprecated functions are declared as deleted instead of only being marked as deprecated, so +code that still calls them no longer compiles. This way, you can find all calls that need to be replaced before version +4.0.0 removes these functions. + +The macro can also be set with the CMake option +[`JSON_DeleteDeprecatedFunctions`](../integration/cmake.md#json_deletedeprecatedfunctions) (`OFF` by default). + +See [full documentation of `JSON_DELETE_DEPRECATED_FUNCTIONS`](../api/macros/json_delete_deprecated_functions.md). + ## `JSON_DIAGNOSTICS` This macro enables extended diagnostics for exception messages. Possible values are `1` to enable or `0` to disable @@ -86,7 +97,8 @@ See [full documentation of `JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json ## `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION` When defined to `1`, a JSON value can no longer be created from a one-element `std::tuple` holding a reference to a JSON -value, such as the result of `std::forward_as_tuple(j)`. This lets `std::tuple` convert such tuples element-wise. +value, such as the result of `std::forward_as_tuple(j)`. This lets `std::tuple` convert such tuples element-wise. This +is planned to become the default in version 4.0.0. See [full documentation of `JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md). diff --git a/docs/mkdocs/docs/integration/cmake.md b/docs/mkdocs/docs/integration/cmake.md index 9151d12be..d75aef47b 100644 --- a/docs/mkdocs/docs/integration/cmake.md +++ b/docs/mkdocs/docs/integration/cmake.md @@ -132,6 +132,12 @@ Build the unit tests when [`BUILD_TESTING`](https://cmake.org/cmake/help/latest/ Enable CI build targets. The exact targets are used during the several CI steps and are subject to change without notice. This option is `OFF` by default. +### `JSON_DeleteDeprecatedFunctions` + +Delete the deprecated functions instead of only deprecating them by defining the macro +[`JSON_DELETE_DEPRECATED_FUNCTIONS`](../api/macros/json_delete_deprecated_functions.md). This option is `OFF` by +default. + ### `JSON_Diagnostics` Enable [extended diagnostic messages](../home/exceptions.md#extended-diagnostic-messages) by defining macro [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md). This option is `OFF` by default. diff --git a/docs/mkdocs/docs/integration/migration_guide.md b/docs/mkdocs/docs/integration/migration_guide.md index 8a07d48a3..17b2eb360 100644 --- a/docs/mkdocs/docs/integration/migration_guide.md +++ b/docs/mkdocs/docs/integration/migration_guide.md @@ -14,6 +14,13 @@ deprecations are annotated with [`HEDLEY_DEPRECATED_FOR`](https://nemequ.github.io/hedley/api-reference.html#HEDLEY_DEPRECATED_FOR) to report which function to use instead. +!!! tip "Find all calls of deprecated functions" + + Define [`JSON_DELETE_DEPRECATED_FUNCTIONS`](../api/macros/json_delete_deprecated_functions.md) to `1` (or set the + CMake option [`JSON_DeleteDeprecatedFunctions`](cmake.md#json_deletedeprecatedfunctions)) to delete all deprecated + functions. Every remaining call then fails to compile, even if deprecation warnings are disabled, so your code is + ready for version 4.0.0 once it compiles with the macro. + ### Parsing - Function `friend std::istream& operator<<(basic_json&, std::istream&)` is deprecated since 3.0.0. Please use @@ -41,8 +48,10 @@ function to use instead. [`from_ubjson`](../api/basic_json/from_ubjson.md), and [`from_bson`](../api/basic_json/from_bson.md)) via initializer lists is deprecated since 3.8.0. Instead, pass two iterators; for instance, call `from_cbor(ptr, ptr+len)` instead of `from_cbor({ptr, len})`. Likewise, passing a pointer and a length as two separate arguments to `from_cbor`, - `from_msgpack`, `from_ubjson`, and `from_bson` is deprecated since 3.8.0; call `from_cbor(ptr, ptr+len)` instead of - `from_cbor(ptr, len)`. + `from_msgpack`, `from_ubjson`, and `from_bson` is deprecated since 3.8.0, and to + [`from_bjdata`](../api/basic_json/from_bjdata.md) and [`from_bon8`](../api/basic_json/from_bon8.md) since 3.13.0; call + `from_cbor(ptr, ptr+len)` instead of `from_cbor(ptr, len)`. These overloads will not be removed in version 4.0.0, but + deleted, so a call like `from_cbor(ptr, len)` cannot compile and convert `len` to the `strict` parameter. === "Deprecated" diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index e2e234929..5b23366bb 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -292,6 +292,7 @@ nav: - 'JSON_BRACE_INIT_COPY_SEMANTICS': api/macros/json_brace_init_copy_semantics.md - 'JSON_CATCH_USER, JSON_THROW_USER, JSON_TRY_USER': api/macros/json_throw_user.md - 'JSON_DIAGNOSTICS': api/macros/json_diagnostics.md + - 'JSON_DELETE_DEPRECATED_FUNCTIONS': api/macros/json_delete_deprecated_functions.md - 'JSON_DIAGNOSTIC_POSITIONS': api/macros/json_diagnostic_positions.md - 'JSON_DISABLE_ENUM_SERIALIZATION': api/macros/json_disable_enum_serialization.md - 'JSON_DISABLE_TUPLE_REFERENCE_CONVERSION': api/macros/json_disable_tuple_reference_conversion.md diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 30edb4a3b..92f52aedc 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -81,9 +81,13 @@ class json_pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_string_t/ JSON_HEDLEY_DEPRECATED_FOR(3.11.0, to_string()) operator string_t() const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return to_string(); } +#endif #ifndef JSON_NO_IO /// @brief write string representation of the JSON pointer to stream @@ -1022,9 +1026,13 @@ class json_pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_eq/ JSON_HEDLEY_DEPRECATED_FOR(3.11.2, operator==(json_pointer)) bool operator==(const string_t& rhs) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return *this == json_pointer(rhs); } +#endif /// @brief 3-way compares two JSON pointers template @@ -1102,18 +1110,26 @@ template& lhs, const StringType& rhs) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return lhs == json_pointer(rhs); } +#endif template::string_t> JSON_HEDLEY_DEPRECATED_FOR(3.11.2, operator==(json_pointer, json_pointer)) inline bool operator==(const StringType& lhs, const json_pointer& rhs) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return json_pointer(lhs) == rhs; } +#endif template inline bool operator!=(const json_pointer& lhs, @@ -1127,18 +1143,26 @@ template& lhs, const StringType& rhs) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return !(lhs == rhs); } +#endif template::string_t> JSON_HEDLEY_DEPRECATED_FOR(3.11.2, operator!=(json_pointer, json_pointer)) inline bool operator!=(const StringType& lhs, const json_pointer& rhs) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return !(lhs == rhs); } +#endif template inline bool operator<(const json_pointer& lhs, diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 17118927c..1a847e1a9 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -888,6 +888,10 @@ #define JSON_DISABLE_ENUM_SERIALIZATION 0 #endif +#ifndef JSON_DELETE_DEPRECATED_FUNCTIONS + #define JSON_DELETE_DEPRECATED_FUNCTIONS 0 +#endif + #ifndef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION #define JSON_DISABLE_TUPLE_REFERENCE_CONVERSION 0 #endif diff --git a/include/nlohmann/detail/macro_unscope.hpp b/include/nlohmann/detail/macro_unscope.hpp index 75bf14c92..494cba0d8 100644 --- a/include/nlohmann/detail/macro_unscope.hpp +++ b/include/nlohmann/detail/macro_unscope.hpp @@ -43,6 +43,7 @@ #undef JSON_PRECISE_STREAM_POSITION #undef JSON_STRICT_NUL_HANDLING #undef JSON_STRICT_BINARY_UTF8 + #undef JSON_DELETE_DEPRECATED_FUNCTIONS #undef JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS #endif diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index ca9682bd5..e1eeac12c 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -3492,9 +3492,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec && !std::is_same>::value, int > = 0 > JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) ValueType value(const ::nlohmann::json_pointer& ptr, const ValueType& default_value) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return value(ptr.convert(), default_value); } +#endif template < class ValueType, class BasicJsonType, class ReturnType = typename value_return_type::type, detail::enable_if_t < @@ -3503,9 +3507,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec && !std::is_same>::value, int > = 0 > JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) ReturnType value(const ::nlohmann::json_pointer& ptr, ValueType && default_value) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return value(ptr.convert(), std::forward(default_value)); } +#endif /// @brief access the first element /// @sa https://json.nlohmann.me/api/basic_json/front/ @@ -3863,9 +3871,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) bool contains(const typename ::nlohmann::json_pointer& ptr) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.contains(this); } +#endif /// @} @@ -3976,9 +3988,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// that is, replace `json::iterator_wrapper(j)` with `j.items()`. JSON_HEDLEY_DEPRECATED_FOR(3.1.0, items()) static iteration_proxy iterator_wrapper(reference ref) noexcept +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ref.items(); } +#endif /// @brief wrapper to access iterator member functions in range-based for /// @sa https://json.nlohmann.me/api/basic_json/items/ @@ -3987,9 +4003,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// that is, replace `json::iterator_wrapper(j)` with `j.items()`. JSON_HEDLEY_DEPRECATED_FOR(3.1.0, items()) static iteration_proxy iterator_wrapper(const_reference ref) noexcept +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ref.items(); } +#endif /// @brief helper to access iterator member functions in range-based for /// @sa https://json.nlohmann.me/api/basic_json/items/ @@ -5307,9 +5327,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// replace calls like `j >> o;` with `o << j;`. JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator<<(std::ostream&, const basic_json&)) friend std::ostream& operator>>(const basic_json& j, std::ostream& o) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return o << j; } +#endif #endif // JSON_NO_IO /// @} @@ -5361,12 +5385,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool allow_exceptions = true, const bool ignore_comments = false, const bool ignore_trailing_commas = false) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { basic_json result; auto p = parser(i.get(), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); p.parse(true, result); return result; } +#endif /// @brief check if the input is valid JSON /// @sa https://json.nlohmann.me/api/basic_json/accept/ @@ -5396,9 +5424,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static bool accept(detail::span_input_adapter&& i, const bool ignore_comments = false, const bool ignore_trailing_commas = false) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return parser(i.get(), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); } +#endif /// @brief generate SAX events /// @sa https://json.nlohmann.me/api/basic_json/sax_parse/ @@ -5459,6 +5491,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool strict = true, const bool ignore_comments = false, const bool ignore_trailing_commas = false) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { auto ia = i.get(); return format == input_format_t::json @@ -5467,6 +5502,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) : detail::binary_reader(std::move(ia), format).sax_parse(sax, strict); } +#endif #if defined(__clang__) #pragma clang diagnostic pop #endif @@ -5479,9 +5515,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// replace calls like `j << i;` with `i >> j;`. JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator>>(std::istream&, basic_json&)) friend std::istream& operator<<(basic_json& j, std::istream& i) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return operator>>(i, j); } +#endif /// @brief deserialize from stream /// @sa https://json.nlohmann.me/api/operator_gtgt/ @@ -5829,9 +5869,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool strict = true, const bool allow_exceptions = true, const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_cbor(ptr, ptr + len, strict, allow_exceptions, tag_handler); } +#endif JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_cbor(ptr, ptr + len)) @@ -5839,9 +5883,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool strict = true, const bool allow_exceptions = true, const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_binary_impl(i.get(), input_format_t::cbor, strict, allow_exceptions, error_handler_t::keep, tag_handler); } +#endif /// @brief create a JSON value from an input in MessagePack format /// @sa https://json.nlohmann.me/api/basic_json/from_msgpack/ @@ -5874,18 +5922,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_msgpack(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_msgpack(ptr, ptr + len, strict, allow_exceptions); } +#endif JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_msgpack(ptr, ptr + len)) static basic_json from_msgpack(detail::span_input_adapter&& i, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_binary_impl(i.get(), input_format_t::msgpack, strict, allow_exceptions); } +#endif /// @brief create a JSON value from an input in UBJSON format /// @sa https://json.nlohmann.me/api/basic_json/from_ubjson/ @@ -5918,18 +5974,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_ubjson(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_ubjson(ptr, ptr + len, strict, allow_exceptions); } +#endif JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_ubjson(ptr, ptr + len)) static basic_json from_ubjson(detail::span_input_adapter&& i, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_binary_impl(i.get(), input_format_t::ubjson, strict, allow_exceptions); } +#endif /// @brief create a JSON value from an input in BJData format /// @sa https://json.nlohmann.me/api/basic_json/from_bjdata/ @@ -5962,9 +6026,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_bjdata(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_bjdata(ptr, ptr + len, strict, allow_exceptions); } +#endif /// @brief create a JSON value from an input in BON8 format /// @sa https://json.nlohmann.me/api/basic_json/from_bon8/ @@ -5995,9 +6063,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_bon8(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_bon8(ptr, ptr + len, strict, allow_exceptions); } +#endif /// @brief create a JSON value from an input in BSON format /// @sa https://json.nlohmann.me/api/basic_json/from_bson/ @@ -6030,18 +6102,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_bson(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_bson(ptr, ptr + len, strict, allow_exceptions); } +#endif JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_bson(ptr, ptr + len)) static basic_json from_bson(detail::span_input_adapter&& i, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_binary_impl(i.get(), input_format_t::bson, strict, allow_exceptions); } +#endif /// @} ////////////////////////// @@ -6061,9 +6141,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec template::value, int> = 0> JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) reference operator[](const ::nlohmann::json_pointer& ptr) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.get_unchecked(this); } +#endif /// @brief access specified element via JSON Pointer /// @sa https://json.nlohmann.me/api/basic_json/operator%5B%5D/ @@ -6075,9 +6159,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec template::value, int> = 0> JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) const_reference operator[](const ::nlohmann::json_pointer& ptr) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.get_unchecked(this); } +#endif /// @brief access specified element via JSON Pointer /// @sa https://json.nlohmann.me/api/basic_json/at/ @@ -6089,9 +6177,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec template::value, int> = 0> JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) reference at(const ::nlohmann::json_pointer& ptr) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.get_checked(this); } +#endif /// @brief access specified element via JSON Pointer /// @sa https://json.nlohmann.me/api/basic_json/at/ @@ -6103,9 +6195,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec template::value, int> = 0> JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) const_reference at(const ::nlohmann::json_pointer& ptr) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.get_checked(this); } +#endif /// @brief return flattened JSON value /// @sa https://json.nlohmann.me/api/basic_json/flatten/ diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index ef6f3d26a..b427b247e 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -3225,6 +3225,10 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define JSON_DISABLE_ENUM_SERIALIZATION 0 #endif +#ifndef JSON_DELETE_DEPRECATED_FUNCTIONS + #define JSON_DELETE_DEPRECATED_FUNCTIONS 0 +#endif + #ifndef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION #define JSON_DISABLE_TUPLE_REFERENCE_CONVERSION 0 #endif @@ -19930,9 +19934,13 @@ class json_pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_string_t/ JSON_HEDLEY_DEPRECATED_FOR(3.11.0, to_string()) operator string_t() const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return to_string(); } +#endif #ifndef JSON_NO_IO /// @brief write string representation of the JSON pointer to stream @@ -20871,9 +20879,13 @@ class json_pointer /// @sa https://json.nlohmann.me/api/json_pointer/operator_eq/ JSON_HEDLEY_DEPRECATED_FOR(3.11.2, operator==(json_pointer)) bool operator==(const string_t& rhs) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return *this == json_pointer(rhs); } +#endif /// @brief 3-way compares two JSON pointers template @@ -20951,18 +20963,26 @@ template& lhs, const StringType& rhs) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return lhs == json_pointer(rhs); } +#endif template::string_t> JSON_HEDLEY_DEPRECATED_FOR(3.11.2, operator==(json_pointer, json_pointer)) inline bool operator==(const StringType& lhs, const json_pointer& rhs) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return json_pointer(lhs) == rhs; } +#endif template inline bool operator!=(const json_pointer& lhs, @@ -20976,18 +20996,26 @@ template& lhs, const StringType& rhs) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return !(lhs == rhs); } +#endif template::string_t> JSON_HEDLEY_DEPRECATED_FOR(3.11.2, operator!=(json_pointer, json_pointer)) inline bool operator!=(const StringType& lhs, const json_pointer& rhs) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return !(lhs == rhs); } +#endif template inline bool operator<(const json_pointer& lhs, @@ -30614,9 +30642,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec && !std::is_same>::value, int > = 0 > JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) ValueType value(const ::nlohmann::json_pointer& ptr, const ValueType& default_value) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return value(ptr.convert(), default_value); } +#endif template < class ValueType, class BasicJsonType, class ReturnType = typename value_return_type::type, detail::enable_if_t < @@ -30625,9 +30657,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec && !std::is_same>::value, int > = 0 > JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) ReturnType value(const ::nlohmann::json_pointer& ptr, ValueType && default_value) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return value(ptr.convert(), std::forward(default_value)); } +#endif /// @brief access the first element /// @sa https://json.nlohmann.me/api/basic_json/front/ @@ -30985,9 +31021,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) bool contains(const typename ::nlohmann::json_pointer& ptr) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.contains(this); } +#endif /// @} @@ -31098,9 +31138,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// that is, replace `json::iterator_wrapper(j)` with `j.items()`. JSON_HEDLEY_DEPRECATED_FOR(3.1.0, items()) static iteration_proxy iterator_wrapper(reference ref) noexcept +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ref.items(); } +#endif /// @brief wrapper to access iterator member functions in range-based for /// @sa https://json.nlohmann.me/api/basic_json/items/ @@ -31109,9 +31153,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// that is, replace `json::iterator_wrapper(j)` with `j.items()`. JSON_HEDLEY_DEPRECATED_FOR(3.1.0, items()) static iteration_proxy iterator_wrapper(const_reference ref) noexcept +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ref.items(); } +#endif /// @brief helper to access iterator member functions in range-based for /// @sa https://json.nlohmann.me/api/basic_json/items/ @@ -32429,9 +32477,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// replace calls like `j >> o;` with `o << j;`. JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator<<(std::ostream&, const basic_json&)) friend std::ostream& operator>>(const basic_json& j, std::ostream& o) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return o << j; } +#endif #endif // JSON_NO_IO /// @} @@ -32483,12 +32535,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool allow_exceptions = true, const bool ignore_comments = false, const bool ignore_trailing_commas = false) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { basic_json result; auto p = parser(i.get(), std::move(cb), allow_exceptions, ignore_comments, ignore_trailing_commas); p.parse(true, result); return result; } +#endif /// @brief check if the input is valid JSON /// @sa https://json.nlohmann.me/api/basic_json/accept/ @@ -32518,9 +32574,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static bool accept(detail::span_input_adapter&& i, const bool ignore_comments = false, const bool ignore_trailing_commas = false) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return parser(i.get(), nullptr, false, ignore_comments, ignore_trailing_commas, true).accept(true); } +#endif /// @brief generate SAX events /// @sa https://json.nlohmann.me/api/basic_json/sax_parse/ @@ -32581,6 +32641,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool strict = true, const bool ignore_comments = false, const bool ignore_trailing_commas = false) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { auto ia = i.get(); return format == input_format_t::json @@ -32589,6 +32652,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) : detail::binary_reader(std::move(ia), format).sax_parse(sax, strict); } +#endif #if defined(__clang__) #pragma clang diagnostic pop #endif @@ -32601,9 +32665,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// replace calls like `j << i;` with `i >> j;`. JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator>>(std::istream&, basic_json&)) friend std::istream& operator<<(basic_json& j, std::istream& i) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return operator>>(i, j); } +#endif /// @brief deserialize from stream /// @sa https://json.nlohmann.me/api/operator_gtgt/ @@ -32951,9 +33019,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool strict = true, const bool allow_exceptions = true, const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_cbor(ptr, ptr + len, strict, allow_exceptions, tag_handler); } +#endif JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_cbor(ptr, ptr + len)) @@ -32961,9 +33033,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const bool strict = true, const bool allow_exceptions = true, const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_binary_impl(i.get(), input_format_t::cbor, strict, allow_exceptions, error_handler_t::keep, tag_handler); } +#endif /// @brief create a JSON value from an input in MessagePack format /// @sa https://json.nlohmann.me/api/basic_json/from_msgpack/ @@ -32996,18 +33072,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_msgpack(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_msgpack(ptr, ptr + len, strict, allow_exceptions); } +#endif JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_msgpack(ptr, ptr + len)) static basic_json from_msgpack(detail::span_input_adapter&& i, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_binary_impl(i.get(), input_format_t::msgpack, strict, allow_exceptions); } +#endif /// @brief create a JSON value from an input in UBJSON format /// @sa https://json.nlohmann.me/api/basic_json/from_ubjson/ @@ -33040,18 +33124,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_ubjson(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_ubjson(ptr, ptr + len, strict, allow_exceptions); } +#endif JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_ubjson(ptr, ptr + len)) static basic_json from_ubjson(detail::span_input_adapter&& i, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_binary_impl(i.get(), input_format_t::ubjson, strict, allow_exceptions); } +#endif /// @brief create a JSON value from an input in BJData format /// @sa https://json.nlohmann.me/api/basic_json/from_bjdata/ @@ -33084,9 +33176,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_bjdata(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_bjdata(ptr, ptr + len, strict, allow_exceptions); } +#endif /// @brief create a JSON value from an input in BON8 format /// @sa https://json.nlohmann.me/api/basic_json/from_bon8/ @@ -33117,9 +33213,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_bon8(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_bon8(ptr, ptr + len, strict, allow_exceptions); } +#endif /// @brief create a JSON value from an input in BSON format /// @sa https://json.nlohmann.me/api/basic_json/from_bson/ @@ -33152,18 +33252,26 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json from_bson(const T* ptr, std::size_t len, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_bson(ptr, ptr + len, strict, allow_exceptions); } +#endif JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_bson(ptr, ptr + len)) static basic_json from_bson(detail::span_input_adapter&& i, const bool strict = true, const bool allow_exceptions = true) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return from_binary_impl(i.get(), input_format_t::bson, strict, allow_exceptions); } +#endif /// @} ////////////////////////// @@ -33183,9 +33291,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec template::value, int> = 0> JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) reference operator[](const ::nlohmann::json_pointer& ptr) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.get_unchecked(this); } +#endif /// @brief access specified element via JSON Pointer /// @sa https://json.nlohmann.me/api/basic_json/operator%5B%5D/ @@ -33197,9 +33309,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec template::value, int> = 0> JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) const_reference operator[](const ::nlohmann::json_pointer& ptr) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.get_unchecked(this); } +#endif /// @brief access specified element via JSON Pointer /// @sa https://json.nlohmann.me/api/basic_json/at/ @@ -33211,9 +33327,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec template::value, int> = 0> JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) reference at(const ::nlohmann::json_pointer& ptr) +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.get_checked(this); } +#endif /// @brief access specified element via JSON Pointer /// @sa https://json.nlohmann.me/api/basic_json/at/ @@ -33225,9 +33345,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec template::value, int> = 0> JSON_HEDLEY_DEPRECATED_FOR(3.11.0, basic_json::json_pointer or nlohmann::json_pointer) // NOLINT(readability/alt_tokens) const_reference at(const ::nlohmann::json_pointer& ptr) const +#if JSON_DELETE_DEPRECATED_FUNCTIONS + = delete; +#else { return ptr.get_checked(this); } +#endif /// @brief return flattened JSON value /// @sa https://json.nlohmann.me/api/basic_json/flatten/ @@ -34387,6 +34511,7 @@ struct formatter // NOLINT(cert-dcl58-c #undef JSON_PRECISE_STREAM_POSITION #undef JSON_STRICT_NUL_HANDLING #undef JSON_STRICT_BINARY_UTF8 + #undef JSON_DELETE_DEPRECATED_FUNCTIONS #undef JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS #endif diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 06646c1d0..5e703a357 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -9,6 +9,14 @@ #include "doctest_compatibility.h" #define JSON_TESTS_PRIVATE +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; @@ -4505,6 +4513,7 @@ TEST_CASE("issue #5648 - from_bjdata(ptr, len) must read len bytes, not treat pt } REQUIRE(contains_nul); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED // before the fix, from_bjdata had no (ptr, len) overload, so this call // bound to from_bjdata(InputType&&, bool strict) instead: ptr was read // as a NUL-terminated C string (stopping at the embedded 0x00 byte), and @@ -4517,4 +4526,5 @@ TEST_CASE("issue #5648 - from_bjdata(ptr, len) must read len bytes, not treat pt // len must not collapse into the strict flag either CHECK(json::from_bjdata(packed.data(), packed.size(), false) == j); +#endif } diff --git a/tests/src/unit-bon8.cpp b/tests/src/unit-bon8.cpp index 9a9116e12..b1d82ca29 100644 --- a/tests/src/unit-bon8.cpp +++ b/tests/src/unit-bon8.cpp @@ -8,6 +8,14 @@ #include "doctest_compatibility.h" +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; #ifdef JSON_TEST_NO_GLOBAL_UDLS @@ -1028,6 +1036,7 @@ TEST_CASE("issue #5648 - from_bon8(ptr, len) must read len bytes, not treat ptr } REQUIRE(contains_nul); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED // before the fix, from_bon8 had no (ptr, len) overload, so this call // bound to from_bon8(InputType&&, bool strict) instead: ptr was read as // a NUL-terminated C string (stopping at the embedded 0x00 byte), and @@ -1040,6 +1049,7 @@ TEST_CASE("issue #5648 - from_bon8(ptr, len) must read len bytes, not treat ptr // len must not collapse into the strict flag either CHECK(json::from_bon8(packed.data(), packed.size(), false) == j); +#endif } #ifdef JSON_HAS_CPP_17 diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index 17819dc65..c9c96b693 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -8,6 +8,14 @@ #include "doctest_compatibility.h" +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; @@ -1261,8 +1269,10 @@ TEST_CASE("BSON input that cannot be read is discarded by every overload") CHECK_THROWS_AS(_ = json::from_bson(input.begin(), input.end()), json::parse_error&); CHECK(json::from_bson(input, true, false).is_discarded()); CHECK(json::from_bson(input.begin(), input.end(), true, false).is_discarded()); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED CHECK(json::from_bson(input.data(), input.size(), true, false).is_discarded()); CHECK(json::from_bson({input.data(), input.size()}, true, false).is_discarded()); +#endif } TEST_CASE("BSON SAX parsing stops at every event") @@ -1740,6 +1750,7 @@ TEST_CASE("BSON roundtrips" * doctest::skip()) CHECK(j1 == j2); } +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED { INFO_WITH_TEMP(filename + ": uint8_t* and size"); // parse JSON file @@ -1754,6 +1765,7 @@ TEST_CASE("BSON roundtrips" * doctest::skip()) // compare parsed JSON values CHECK(j1 == j2); } +#endif { INFO_WITH_TEMP(filename + ": output to output adapters"); diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 546e256b1..1f584d3b3 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -8,6 +8,14 @@ #include "doctest_compatibility.h" +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; @@ -2215,8 +2223,10 @@ TEST_CASE("CBOR input that cannot be read is discarded by every overload") CHECK_THROWS_AS(_ = json::from_cbor(input.begin(), input.end()), json::parse_error&); CHECK(json::from_cbor(input, true, false).is_discarded()); CHECK(json::from_cbor(input.begin(), input.end(), true, false).is_discarded()); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED CHECK(json::from_cbor(input.data(), input.size(), true, false).is_discarded()); CHECK(json::from_cbor({input.data(), input.size()}, true, false).is_discarded()); +#endif // a string that ends early, read through iterators that are not // contiguous and have to be copied from one element at a time @@ -2613,12 +2623,14 @@ TEST_CASE("CBOR roundtrips" * doctest::skip()) CHECK(j1 == j2); } +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED { INFO_WITH_TEMP(filename + ": uint8_t* and size"); json j2; CHECK_NOTHROW(j2 = json::from_cbor({packed.data(), packed.size()})); CHECK(j1 == j2); } +#endif { INFO_WITH_TEMP(filename + ": output to output adapters"); diff --git a/tests/src/unit-delete_deprecated_functions.cpp b/tests/src/unit-delete_deprecated_functions.cpp new file mode 100644 index 000000000..f54e9966a --- /dev/null +++ b/tests/src/unit-delete_deprecated_functions.cpp @@ -0,0 +1,214 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +// This file tests the opt-in JSON_DELETE_DEPRECATED_FUNCTIONS, so it defines the +// macro itself instead of relying on the build configuration. +#ifdef JSON_DELETE_DEPRECATED_FUNCTIONS + #undef JSON_DELETE_DEPRECATED_FUNCTIONS +#endif +#define JSON_DELETE_DEPRECATED_FUNCTIONS 1 + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +template +struct make_void +{ + using type = void; +}; + +template +using void_t = typename make_void::type; + +// A deleted function is still found by overload resolution, but naming it in an +// unevaluated operand is ill-formed, so each of the following traits is false +// exactly if the expression selects a deleted (or no) function. +#define JSON_TEST_DETECT(name, expr) \ + template \ + struct name : std::false_type {}; \ + template \ + struct name> : std::true_type {} + +using ptr_t = const std::uint8_t*; + +JSON_TEST_DETECT(from_cbor_ptr_len, J::from_cbor(std::declval(), std::declval())); +JSON_TEST_DETECT(from_msgpack_ptr_len, J::from_msgpack(std::declval(), std::declval())); +JSON_TEST_DETECT(from_ubjson_ptr_len, J::from_ubjson(std::declval(), std::declval())); +JSON_TEST_DETECT(from_bjdata_ptr_len, J::from_bjdata(std::declval(), std::declval())); +JSON_TEST_DETECT(from_bon8_ptr_len, J::from_bon8(std::declval(), std::declval())); +JSON_TEST_DETECT(from_bson_ptr_len, J::from_bson(std::declval(), std::declval())); + +JSON_TEST_DETECT(from_cbor_ptr_ptr, J::from_cbor(std::declval(), std::declval())); +JSON_TEST_DETECT(from_msgpack_ptr_ptr, J::from_msgpack(std::declval(), std::declval())); +JSON_TEST_DETECT(from_ubjson_ptr_ptr, J::from_ubjson(std::declval(), std::declval())); +JSON_TEST_DETECT(from_bjdata_ptr_ptr, J::from_bjdata(std::declval(), std::declval())); +JSON_TEST_DETECT(from_bon8_ptr_ptr, J::from_bon8(std::declval(), std::declval())); +JSON_TEST_DETECT(from_bson_ptr_ptr, J::from_bson(std::declval(), std::declval())); + +JSON_TEST_DETECT(from_cbor_init_list, J::from_cbor({std::declval(), std::declval()})); +JSON_TEST_DETECT(from_msgpack_init_list, J::from_msgpack({std::declval(), std::declval()})); +JSON_TEST_DETECT(from_ubjson_init_list, J::from_ubjson({std::declval(), std::declval()})); +JSON_TEST_DETECT(from_bson_init_list, J::from_bson({std::declval(), std::declval()})); + +JSON_TEST_DETECT(parse_init_list, J::parse({std::declval(), std::declval()})); +JSON_TEST_DETECT(accept_init_list, J::accept({std::declval(), std::declval()})); +JSON_TEST_DETECT(sax_parse_init_list, J::sax_parse({std::declval(), std::declval()}, std::declval*>())); +JSON_TEST_DETECT(parse_ptr_ptr, J::parse(std::declval(), std::declval())); + +JSON_TEST_DETECT(iterator_wrapper, J::iterator_wrapper(std::declval())); +JSON_TEST_DETECT(iterator_wrapper_const, J::iterator_wrapper(std::declval())); +JSON_TEST_DETECT(items, std::declval().items()); + +JSON_TEST_DETECT(json_ltlt_istream, std::declval() << std::declval()); +JSON_TEST_DETECT(istream_gtgt_json, std::declval() >> std::declval()); +JSON_TEST_DETECT(json_gtgt_ostream, std::declval() >> std::declval()); +JSON_TEST_DETECT(ostream_ltlt_json, std::declval() << std::declval()); + +JSON_TEST_DETECT(ptr_eq_ptr, std::declval() == std::declval()); +JSON_TEST_DETECT(ptr_ne_ptr, std::declval() != std::declval()); +JSON_TEST_DETECT(ptr_eq_string, std::declval() == std::declval()); +JSON_TEST_DETECT(string_eq_ptr, std::declval() == std::declval()); +JSON_TEST_DETECT(ptr_eq_c_string, std::declval() == std::declval()); +JSON_TEST_DETECT(c_string_eq_ptr, std::declval() == std::declval()); +JSON_TEST_DETECT(ptr_ne_string, std::declval() != std::declval()); +JSON_TEST_DETECT(string_ne_ptr, std::declval() != std::declval()); +JSON_TEST_DETECT(ptr_to_string, std::declval().to_string()); + +// json_pointer with a basic_json type as template argument +template +using legacy_ptr_t = const nlohmann::json_pointer& ; +JSON_TEST_DETECT(legacy_ptr_value, std::declval().value(std::declval>(), 0)); +JSON_TEST_DETECT(legacy_ptr_value_rvalue, std::declval().value(std::declval>(), std::string())); +JSON_TEST_DETECT(legacy_ptr_contains, std::declval().contains(std::declval>())); +JSON_TEST_DETECT(legacy_ptr_subscript, std::declval()[std::declval>()]); +JSON_TEST_DETECT(legacy_ptr_subscript_const, std::declval()[std::declval>()]); +JSON_TEST_DETECT(legacy_ptr_at, std::declval().at(std::declval>())); +JSON_TEST_DETECT(legacy_ptr_at_const, std::declval().at(std::declval>())); +JSON_TEST_DETECT(ptr_value, std::declval().value(std::declval(), 0)); +JSON_TEST_DETECT(ptr_at, std::declval().at(std::declval())); + +#undef JSON_TEST_DETECT +} // namespace + +TEST_CASE("JSON_DELETE_DEPRECATED_FUNCTIONS") +{ + SECTION("from_* with a pointer and a length") + { + // the overloads are deleted rather than removed, so the length cannot + // silently bind to the strict parameter of from_*(InputType&&, bool) + CHECK_FALSE(from_cbor_ptr_len::value); + CHECK_FALSE(from_msgpack_ptr_len::value); + CHECK_FALSE(from_ubjson_ptr_len::value); + CHECK_FALSE(from_bjdata_ptr_len::value); + CHECK_FALSE(from_bon8_ptr_len::value); + CHECK_FALSE(from_bson_ptr_len::value); + + // the replacement + CHECK(from_cbor_ptr_ptr::value); + CHECK(from_msgpack_ptr_ptr::value); + CHECK(from_ubjson_ptr_ptr::value); + CHECK(from_bjdata_ptr_ptr::value); + CHECK(from_bon8_ptr_ptr::value); + CHECK(from_bson_ptr_ptr::value); + } + + SECTION("initializer lists of a pointer and a length") + { + CHECK_FALSE(from_cbor_init_list::value); + CHECK_FALSE(from_msgpack_init_list::value); + CHECK_FALSE(from_ubjson_init_list::value); + CHECK_FALSE(from_bson_init_list::value); + CHECK_FALSE(parse_init_list::value); + CHECK_FALSE(accept_init_list::value); + CHECK_FALSE(sax_parse_init_list::value); + + // the replacement + CHECK(parse_ptr_ptr::value); + } + + SECTION("iterator_wrapper") + { + CHECK_FALSE(iterator_wrapper::value); + CHECK_FALSE(iterator_wrapper_const::value); + + // the replacement + CHECK(items::value); + } + + SECTION("stream operators with reversed operands") + { + CHECK_FALSE(json_ltlt_istream::value); + CHECK_FALSE(json_gtgt_ostream::value); + + // the replacement + CHECK(istream_gtgt_json::value); + CHECK(ostream_ltlt_json::value); + } + + SECTION("json_pointer") + { + // conversion to a string + CHECK_FALSE(std::is_constructible::value); + CHECK_FALSE(std::is_convertible::value); + + // comparison with a string + CHECK_FALSE(ptr_eq_string::value); + CHECK_FALSE(string_eq_ptr::value); + CHECK_FALSE(ptr_eq_c_string::value); + CHECK_FALSE(c_string_eq_ptr::value); + CHECK_FALSE(ptr_ne_string::value); + CHECK_FALSE(string_ne_ptr::value); + + // the replacement + CHECK(ptr_to_string::value); + CHECK(ptr_eq_ptr::value); + CHECK(ptr_ne_ptr::value); + } + + SECTION("json_pointer with a basic_json type as template argument") + { + CHECK_FALSE(legacy_ptr_value::value); + CHECK_FALSE(legacy_ptr_value_rvalue::value); + CHECK_FALSE(legacy_ptr_contains::value); + CHECK_FALSE(legacy_ptr_subscript::value); + CHECK_FALSE(legacy_ptr_subscript_const::value); + CHECK_FALSE(legacy_ptr_at::value); + CHECK_FALSE(legacy_ptr_at_const::value); + + // the replacement + CHECK(ptr_value::value); + CHECK(ptr_at::value); + } + + SECTION("the non-deprecated functions still work") + { + const json j = {{"a", {1, 2}}}; + const auto cbor = json::to_cbor(j); + CHECK(json::from_cbor(cbor.data(), cbor.data() + cbor.size()) == j); + + const json::json_pointer ptr("/a/1"); + CHECK(ptr == json::json_pointer("/a/1")); + CHECK(ptr.to_string() == "/a/1"); + CHECK(j[ptr] == 2); + CHECK(j.at(ptr) == 2); + CHECK(j.value(ptr, 0) == 2); + CHECK(j.contains(ptr)); + } +} diff --git a/tests/src/unit-deserialization.cpp b/tests/src/unit-deserialization.cpp index 2f9ff63fa..ca9b63112 100644 --- a/tests/src/unit-deserialization.cpp +++ b/tests/src/unit-deserialization.cpp @@ -16,6 +16,14 @@ #define JSON_TEST_STRICT_NUL_HANDLING_ENABLED 1 #endif +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; #ifdef JSON_TEST_NO_GLOBAL_UDLS @@ -291,6 +299,7 @@ TEST_CASE("deserialization") })); } +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED SECTION("operator<<") { std::stringstream ss; @@ -299,6 +308,7 @@ TEST_CASE("deserialization") j << ss; CHECK(j == json({"foo", 1, 2, 3, false, {{"one", 1}}})); } +#endif SECTION("operator>>") { @@ -423,6 +433,7 @@ TEST_CASE("deserialization") CHECK_THROWS_WITH_AS(_ = json::parse(nullptr), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&); } +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED SECTION("operator<<") { std::stringstream ss; @@ -430,6 +441,7 @@ TEST_CASE("deserialization") json j; CHECK_THROWS_WITH_AS(j << ss, "[json.exception.parse_error.101] parse error at line 1, column 29: syntax error while parsing array - unexpected end of input; expected ']'", json::parse_error&); } +#endif SECTION("operator>>") { @@ -1148,9 +1160,9 @@ TEST_CASE("deserialization") { std::istringstream s(bom + "123 456"); json j; - j << s; + s >> j; CHECK(j == 123); - j << s; + s >> j; CHECK(j == 456); } } diff --git a/tests/src/unit-items.cpp b/tests/src/unit-items.cpp index 81959db8d..71b0522a8 100644 --- a/tests/src/unit-items.cpp +++ b/tests/src/unit-items.cpp @@ -8,6 +8,14 @@ #include "doctest_compatibility.h" +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; @@ -19,6 +27,7 @@ DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING("-Wrange-loop-construct") +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED TEST_CASE("iterator_wrapper") { SECTION("object") @@ -715,6 +724,7 @@ TEST_CASE("iterator_wrapper") } } } +#endif TEST_CASE("items()") { diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 86c636c2a..b4ed2c9cc 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -9,6 +9,14 @@ #include "doctest_compatibility.h" #define JSON_TESTS_PRIVATE +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; #ifdef JSON_TEST_NO_GLOBAL_UDLS @@ -564,7 +572,9 @@ TEST_CASE("JSON pointers") std::stringstream ss; ss << ptr; CHECK(ptr.to_string() == ptr_str); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED CHECK(std::string(ptr) == ptr_str); +#endif CHECK(ss.str() == ptr_str); } } @@ -744,8 +754,6 @@ TEST_CASE("JSON pointers") SECTION("equality comparison") { - const char* ptr_cpstring = "/foo/bar"; - const char ptr_castring[] = "/foo/bar"; // NOLINT(misc-const-correctness,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-avoid-c-arrays) std::string ptr_string{"/foo/bar"}; auto ptr1 = json::json_pointer(ptr_string); auto ptr2 = json::json_pointer(ptr_string); @@ -755,6 +763,12 @@ TEST_CASE("JSON pointers") CHECK(ptr1 == ptr2); + CHECK_FALSE(ptr1 != ptr2); + +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED + const char* ptr_cpstring = "/foo/bar"; + const char ptr_castring[] = "/foo/bar"; // NOLINT(misc-const-correctness,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-avoid-c-arrays) + CHECK(ptr1 == "/foo/bar"); CHECK(ptr1 == ptr_cpstring); CHECK(ptr1 == ptr_castring); @@ -765,8 +779,6 @@ TEST_CASE("JSON pointers") CHECK(ptr_castring == ptr1); CHECK(ptr_string == ptr1); - CHECK_FALSE(ptr1 != ptr2); - CHECK_FALSE(ptr1 != "/foo/bar"); CHECK_FALSE(ptr1 != ptr_cpstring); CHECK_FALSE(ptr1 != ptr_castring); @@ -788,6 +800,7 @@ TEST_CASE("JSON pointers") CHECK_THROWS_WITH_AS("/~~" == ptr1, "[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&); } +#endif } SECTION("less-than comparison") @@ -839,6 +852,7 @@ TEST_CASE("JSON pointers") json_ptr_j ptr_j{ptr_string}; json_ptr_oj ptr_oj{ptr_string}; +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED CHECK(j.contains(ptr)); CHECK(j.contains(ptr_j)); CHECK(j.contains(ptr_oj)); @@ -851,6 +865,7 @@ TEST_CASE("JSON pointers") CHECK(j.value(ptr, "x") == j.value(ptr_j, "x")); CHECK(j.value(ptr, "x") == j.value(ptr_oj, "x")); +#endif CHECK(ptr == ptr_j); CHECK(ptr == ptr_oj); diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index c4b447d64..d0db8438c 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -8,6 +8,14 @@ #include "doctest_compatibility.h" +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; #ifdef JSON_TEST_NO_GLOBAL_UDLS @@ -1795,8 +1803,10 @@ TEST_CASE("MessagePack input that cannot be read is discarded by every overload" CHECK_THROWS_AS(_ = json::from_msgpack(input.begin(), input.end()), json::parse_error&); CHECK(json::from_msgpack(input, true, false).is_discarded()); CHECK(json::from_msgpack(input.begin(), input.end(), true, false).is_discarded()); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED CHECK(json::from_msgpack(input.data(), input.size(), true, false).is_discarded()); CHECK(json::from_msgpack({input.data(), input.size()}, true, false).is_discarded()); +#endif } TEST_CASE("MessagePack SAX parsing stops at every event") @@ -2099,12 +2109,14 @@ TEST_CASE("MessagePack roundtrips" * doctest::skip()) CHECK(j1 == j2); } +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED { INFO_WITH_TEMP(filename + ": uint8_t* and size"); json j2; CHECK_NOTHROW(j2 = json::from_msgpack({packed.data(), packed.size()})); CHECK(j1 == j2); } +#endif { INFO_WITH_TEMP(filename + ": output to output adapters"); diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 79ca5a770..1a8c73d4e 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -569,7 +569,7 @@ TEST_CASE("regression tests 2") SECTION("issue #2067 - cannot serialize binary data to text JSON") { const std::array data = {{0x81, 0xA4, 0x64, 0x61, 0x74, 0x61, 0xC4, 0x0F, 0x33, 0x30, 0x30, 0x32, 0x33, 0x34, 0x30, 0x31, 0x30, 0x37, 0x30, 0x35, 0x30, 0x31, 0x30}}; - const json j = json::from_msgpack(data.data(), data.size()); + const json j = json::from_msgpack(data.begin(), data.end()); // dump() is nodiscard; this only checks that dumping does not throw CHECK_NOTHROW( utils::ignore_return_value( diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index 4b959021e..4b7a53b10 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -8,6 +8,14 @@ #include "doctest_compatibility.h" +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; @@ -48,6 +56,7 @@ TEST_CASE("serialization") } } +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED SECTION("operator>>") { SECTION("no given width") @@ -79,6 +88,7 @@ TEST_CASE("serialization") "[\n\t\"foo\",\n\t1,\n\t2,\n\t3,\n\tfalse,\n\t{\n\t\t\"one\": 1\n\t}\n]"); } } +#endif SECTION("dump") { diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index 80ca214a4..a4060bd53 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -8,6 +8,14 @@ #include "doctest_compatibility.h" +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #include using nlohmann::json; @@ -2212,8 +2220,10 @@ TEST_CASE("UBJSON input that cannot be read is discarded by every overload") CHECK_THROWS_AS(_ = json::from_ubjson(input.begin(), input.end()), json::parse_error&); CHECK(json::from_ubjson(input, true, false).is_discarded()); CHECK(json::from_ubjson(input.begin(), input.end(), true, false).is_discarded()); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED CHECK(json::from_ubjson(input.data(), input.size(), true, false).is_discarded()); CHECK(json::from_ubjson({input.data(), input.size()}, true, false).is_discarded()); +#endif } TEST_CASE("UBJSON SAX parsing stops at every event") @@ -2961,12 +2971,14 @@ TEST_CASE("UBJSON roundtrips" * doctest::skip()) CHECK(j1 == j2); } +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED { INFO_WITH_TEMP(filename + ": uint8_t* and size"); json j2; CHECK_NOTHROW(j2 = json::from_ubjson({packed.data(), packed.size()})); CHECK(j1 == j2); } +#endif { INFO_WITH_TEMP(filename + ": output to output adapters"); From 8a142bc4362afa543df5cd52f35a1b597b973a25 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 6 Oct 2026 07:22:55 +0200 Subject: [PATCH 4/6] Fix CI on develop after #5600, #5607, and #5755 (#5764) * Fix CI on develop after #5600, #5607, and #5755 - binary_reader: cast the result of -1 - number back to number_integer_t, because a number_integer_t narrower than int is promoted to int, which GCC's -Warith-conversion rejects (ci_test_gcc, ci_test_standards_gcc) - JSON_DELETE_DEPRECATED_FUNCTIONS: declare the deleted stream operators as function templates at namespace scope, because GCC < 5 rejects deleted friend functions ("can't initialize friend function") and Clang 7-9 report a redefinition when a class template has a deleted friend function - docs: give the examples of JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS "Example:" titles and add the page to the docset (style_check) Signed-off-by: Niels Lohmann * Ignore libstdc++'s sign change in the sanitizer job libstdc++ 14's initializes a size_t parameter with -1 (GCC bug 119429), so every std::format call fails ci_test_clang_sanitizer under -fsanitize=integer (test-std-format_cpp20). Exclude only the implicit-integer-sign-change check and only that header via -fsanitize-ignorelist. Signed-off-by: Niels Lohmann * Fix the AppVeyor (MSVC 2015-2019) build - binary_reader: emit_signed/emit_unsigned pass integers that do not fit the number types to emit_float as long double, because MSVC's has no integer overloads of std::isfinite (C2668 'fpclassify'), from #5607 - scalar comparisons: the friend operators take the JSON type for their noexcept from their parameter, because MSVC 2015/2017 take basic_json as the class template there (C3203) and MSVC 2019 16.0 does not see member types or template parameters, from #5751 - unit-conversions2: skip the !is_nothrow_constructible static_assert for std::optional on MSVC 2017, which evaluates the conditional noexcept as true (C2607), from #5754 Signed-off-by: Niels Lohmann * Re-amalgamate Signed-off-by: Niels Lohmann * Avoid MSVC 2015's C4800 for enums with underlying type bool MSVC 2015 warns about any conversion to bool (C4800), even with an explicit cast, so the enum conversions from #5754 (#5671) failed the AppVeyor build with /WX. Convert to bool by comparing with zero via the new detail::bool_aware_static_cast, and keep doctest from printing the enum in the test. Signed-off-by: Niels Lohmann * Skip the #5650 allocator test on MSVC 2015 debug builds MSVC 2015's debug STL constructs the containers' debug proxies through the allocator in noexcept constructors, so countdown_allocator's failing construction crashes test-allocator (SIGSEGV) instead of throwing std::bad_alloc. Use the guard #5585 uses for the same reason. Signed-off-by: Niels Lohmann * Skip deleted-function detection checks on MSVC 2015 MSVC 2015 does not treat selecting a deleted function in decltype as a substitution failure, so the detection traits in unit-delete_deprecated_functions (#5755) and the integral-key checks in unit-element_access2 (#5657) report deleted overloads as callable there. Calling them still fails to compile. Skip those checks for _MSC_VER < 1910, and use the stream operators for real in the runtime section, so MSVC 2015 still compiles them with the macro set. Signed-off-by: Niels Lohmann * Split the Visual Studio 2017 AppVeyor jobs in two The VS 2017 jobs hit AppVeyor's 60-minute limit per job while still compiling the tests (77 of about 108 test targets after 58 minutes). They pass /std:c++17 for everything anyway, so build only the C++17 variant of each test (JSON_TestStandards=17), and split the unit test files across two jobs each with the new JSON_TestShard=/ option, which keeps every -th test file starting at . The extra variants of single test files are built in shard 0 only. CMAKE_OPTIONS is no longer quoted in appveyor.yml, so that it can hold more than one option. Signed-off-by: Niels Lohmann * Fix std::terminate when converting std::optional with MSVC 2017 MSVC 2017 evaluates std::is_nothrow_assignable as true even if T's to_json throws, so to_json(json&, const std::optional&) was noexcept there and the exception from #5642's test called std::terminate instead of propagating. Make that conversion never noexcept on MSVC 2017; all other compilers keep the exact condition. The static_asserts on the condition are skipped for MSVC 2017; the runtime check that the exception propagates still runs there. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .github/external_ci/appveyor.yml | 21 +++- cmake/ci.cmake | 2 +- cmake/clang_sanitizer_ignorelist.txt | 8 ++ docs/docset/docSet.sql | 1 + .../json_use_objects_for_enum_keyed_maps.md | 4 +- .../nlohmann/detail/conversions/from_json.hpp | 2 +- .../nlohmann/detail/conversions/to_json.hpp | 15 ++- .../nlohmann/detail/input/binary_reader.hpp | 14 ++- include/nlohmann/detail/meta/type_traits.hpp | 15 +++ include/nlohmann/json.hpp | 57 ++++++---- single_include/nlohmann/json.hpp | 103 +++++++++++++----- tests/CMakeLists.txt | 33 ++++++ tests/src/unit-allocator.cpp | 5 + tests/src/unit-conversions2.cpp | 9 +- .../src/unit-delete_deprecated_functions.cpp | 15 +++ tests/src/unit-element_access2.cpp | 5 + 16 files changed, 245 insertions(+), 64 deletions(-) create mode 100644 cmake/clang_sanitizer_ignorelist.txt diff --git a/.github/external_ci/appveyor.yml b/.github/external_ci/appveyor.yml index 805195742..4f0d3f5e3 100644 --- a/.github/external_ci/appveyor.yml +++ b/.github/external_ci/appveyor.yml @@ -16,6 +16,9 @@ only_commits: environment: matrix: + # The Visual Studio 2017 jobs compile everything with /std:c++17, so they + # only build the C++17 variant of each test, split into two jobs each to + # stay below AppVeyor's 60-minute limit per job. - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 configuration: Debug platform: x86 @@ -34,7 +37,13 @@ environment: configuration: Release platform: x86 CXX_FLAGS: "/permissive- /std:c++17 /utf-8 /W4 /WX" - CMAKE_OPTIONS: "" + CMAKE_OPTIONS: "-DJSON_TestStandards=17 -DJSON_TestShard=0/2" + GENERATOR: Visual Studio 15 2017 + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + configuration: Release + platform: x86 + CXX_FLAGS: "/permissive- /std:c++17 /utf-8 /W4 /WX" + CMAKE_OPTIONS: "-DJSON_TestStandards=17 -DJSON_TestShard=1/2" GENERATOR: Visual Studio 15 2017 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 @@ -55,7 +64,13 @@ environment: configuration: Release platform: x64 CXX_FLAGS: "/permissive- /std:c++17 /Zc:__cplusplus /utf-8 /W4 /WX" - CMAKE_OPTIONS: "" + CMAKE_OPTIONS: "-DJSON_TestStandards=17 -DJSON_TestShard=0/2" + GENERATOR: Visual Studio 15 2017 + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + configuration: Release + platform: x64 + CXX_FLAGS: "/permissive- /std:c++17 /Zc:__cplusplus /utf-8 /W4 /WX" + CMAKE_OPTIONS: "-DJSON_TestStandards=17 -DJSON_TestShard=1/2" GENERATOR: Visual Studio 15 2017 init: @@ -66,7 +81,7 @@ install: - if "%platform%"=="x86" set GENERATOR_PLATFORM=Win32 before_build: - - cmake . -G "%GENERATOR%" -A "%GENERATOR_PLATFORM%" -DCMAKE_CXX_FLAGS="%CXX_FLAGS%" -DCMAKE_IGNORE_PATH="C:/Program Files/Git/usr/bin" -DJSON_BuildTests=On "%CMAKE_OPTIONS%" + - cmake . -G "%GENERATOR%" -A "%GENERATOR_PLATFORM%" -DCMAKE_CXX_FLAGS="%CXX_FLAGS%" -DCMAKE_IGNORE_PATH="C:/Program Files/Git/usr/bin" -DJSON_BuildTests=On %CMAKE_OPTIONS% build_script: - cmake --build . --config "%configuration%" --parallel 2 diff --git a/cmake/ci.cmake b/cmake/ci.cmake index b9d246bbf..c4bced5ba 100644 --- a/cmake/ci.cmake +++ b/cmake/ci.cmake @@ -376,7 +376,7 @@ add_custom_target(ci_test_coverage # Sanitizers. ############################################################################### -set(CLANG_CXX_FLAGS_SANITIZER "-g -O1 -fsanitize=address -fsanitize=undefined -fsanitize=integer -fsanitize=nullability -fno-omit-frame-pointer -fno-sanitize-recover=all -fno-sanitize=unsigned-integer-overflow -fno-sanitize=unsigned-shift-base") +set(CLANG_CXX_FLAGS_SANITIZER "-g -O1 -fsanitize=address -fsanitize=undefined -fsanitize=integer -fsanitize=nullability -fno-omit-frame-pointer -fno-sanitize-recover=all -fno-sanitize=unsigned-integer-overflow -fno-sanitize=unsigned-shift-base -fsanitize-ignorelist=${PROJECT_SOURCE_DIR}/cmake/clang_sanitizer_ignorelist.txt") add_custom_target(ci_test_clang_sanitizer COMMAND CXX=${CLANG_TOOL} CXXFLAGS=${CLANG_CXX_FLAGS_SANITIZER} ${CMAKE_COMMAND} diff --git a/cmake/clang_sanitizer_ignorelist.txt b/cmake/clang_sanitizer_ignorelist.txt new file mode 100644 index 000000000..153b6a76d --- /dev/null +++ b/cmake/clang_sanitizer_ignorelist.txt @@ -0,0 +1,8 @@ +# Sanitizer ignore list for ci_test_clang_sanitizer (-fsanitize-ignorelist). +# +# libstdc++ 14's declares `_Scanner(basic_string_view<_CharT>, size_t __nargs = -1)`, so every std::format +# call converts -1 to size_t, which -fsanitize=integer reports as implicit-integer-sign-change. This is +# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=119429, not a bug in this library. Only that check and only are +# excluded, so implicit sign changes in the library and the tests are still reported. +[implicit-integer-sign-change] +src:*/include/c++/*/format diff --git a/docs/docset/docSet.sql b/docs/docset/docSet.sql index 9fc5f7e3d..8e956413c 100644 --- a/docs/docset/docSet.sql +++ b/docs/docset/docSet.sql @@ -250,6 +250,7 @@ INSERT INTO searchIndex(name, type, path) VALUES ('JSON_TRY_USER', 'Macro', 'api INSERT INTO searchIndex(name, type, path) VALUES ('JSON_USE_GLOBAL_UDLS', 'Macro', 'api/macros/json_use_global_udls/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('JSON_USE_IMPLICIT_CONVERSIONS', 'Macro', 'api/macros/json_use_implicit_conversions/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON', 'Macro', 'api/macros/json_use_legacy_discarded_value_comparison/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS', 'Macro', 'api/macros/json_use_objects_for_enum_keyed_maps/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('JSON_USE_SIMDUTF', 'Macro', 'api/macros/json_use_simdutf/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('Macros', 'Macro', 'api/macros/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE', 'Macro', 'api/macros/nlohmann_define_derived_type/index.html'); diff --git a/docs/mkdocs/docs/api/macros/json_use_objects_for_enum_keyed_maps.md b/docs/mkdocs/docs/api/macros/json_use_objects_for_enum_keyed_maps.md index a84d83129..ca4139c48 100644 --- a/docs/mkdocs/docs/api/macros/json_use_objects_for_enum_keyed_maps.md +++ b/docs/mkdocs/docs/api/macros/json_use_objects_for_enum_keyed_maps.md @@ -69,7 +69,7 @@ The default value is `0` (disabled — existing behavior is preserved). ## Examples -??? example "Default behavior (macro not defined)" +??? example "Example: default behavior (macro not defined)" Without the macro, a map with enum keys is stored as an array of pairs: @@ -96,7 +96,7 @@ The default value is `0` (disabled — existing behavior is preserved). } ``` -??? example "Objects for enum-keyed maps (macro defined to 1)" +??? example "Example: objects for enum-keyed maps (macro defined to 1)" With the macro, the same map is stored as an object: diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 53f6d01cf..884ff014a 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -172,7 +172,7 @@ inline void from_json(const BasicJsonType& j, EnumType& e) typename BasicJsonType::number_unsigned_t, underlying_type>::type; value_type val; get_arithmetic_value(j, val); - e = static_cast(static_cast(val)); + e = static_cast(bool_aware_static_cast(val)); } #endif // JSON_DISABLE_ENUM_SERIALIZATION diff --git a/include/nlohmann/detail/conversions/to_json.hpp b/include/nlohmann/detail/conversions/to_json.hpp index 5b332d4e9..f3b4994f1 100644 --- a/include/nlohmann/detail/conversions/to_json.hpp +++ b/include/nlohmann/detail/conversions/to_json.hpp @@ -287,9 +287,20 @@ struct external_constructor ///////////// #ifdef JSON_HAS_CPP_17 +// whether storing the value of a std::optional cannot throw; MSVC 2017 +// evaluates std::is_nothrow_assignable as true even if T's to_json throws, so +// the exception would call std::terminate (#5642) +#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER < 1920 + template + using is_nothrow_optional_to_json = std::false_type; +#else + template + using is_nothrow_optional_to_json = std::is_nothrow_assignable; +#endif + template::value, int> = 0> -void to_json(BasicJsonType& j, const std::optional& opt) noexcept(std::is_nothrow_assignable::value) +void to_json(BasicJsonType& j, const std::optional& opt) noexcept(is_nothrow_optional_to_json::value) { if (opt.has_value()) { @@ -365,7 +376,7 @@ inline void to_json(BasicJsonType& j, EnumType e) noexcept { using underlying_type = typename std::underlying_type::type; static constexpr value_t integral_value_t = std::is_unsigned::value ? value_t::number_unsigned : value_t::number_integer; - external_constructor::construct(j, static_cast(e)); + external_constructor::construct(j, bool_aware_static_cast(e)); } #endif // JSON_DISABLE_ENUM_SERIALIZATION diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 369e274fb..e6c698fa0 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -662,10 +662,11 @@ class binary_reader } // the value is -1 - number, which fits into number_integer_t - // whenever number does + // whenever number does; the outer cast undoes the integral promotion + // for number_integer_t types narrower than int if (JSON_HEDLEY_LIKELY(value_in_range_of(number))) { - return sax->number_integer(static_cast(-1) - static_cast(number)); + return sax->number_integer(conditional_static_cast(static_cast(-1) - static_cast(number))); } // like the lexer does for JSON text, store a value too small for @@ -4003,7 +4004,8 @@ class binary_reader { return sax->number_unsigned(static_cast(number)); } - return emit_float(number); + // std::isfinite has no integer overloads in MSVC's + return emit_float(static_cast(number)); } /*! @@ -4026,7 +4028,8 @@ class binary_reader { return sax->number_unsigned(static_cast(number)); } - return emit_float(number); + // std::isfinite has no integer overloads in MSVC's + return emit_float(static_cast(number)); } /*! @@ -4037,7 +4040,8 @@ class binary_reader and NaN in the input are passed on unchanged. Integers only overflow if number_float_t cannot represent 2^64, e.g., a half-precision type. - @tparam NumberType a floating-point or integer type + @tparam NumberType a floating-point type (emit_signed and emit_unsigned + convert integers to long double first) @param[in] number the number @return whether the SAX parser accepted the value diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 69ab05f4e..4c90c68a3 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -906,6 +906,21 @@ T conditional_static_cast(U value) return value; } +// like conditional_static_cast, but converts to bool by comparing with zero, +// because MSVC 2015 warns about any conversion to bool (C4800), even with an +// explicit cast; used for enums whose underlying type is bool +template < typename T, typename U, enable_if_t < !std::is_same::value, int > = 0 > +T bool_aware_static_cast(U value) +{ + return conditional_static_cast(value); +} + +template::value, int> = 0> +bool bool_aware_static_cast(U value) +{ + return value != U(); +} + template using all_integral = conjunction...>; diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index e1eeac12c..6f6bb8d04 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -5098,7 +5098,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_le/ template requires std::is_scalar_v - friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) <= rhs; } @@ -5107,7 +5107,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ge/ template requires std::is_scalar_v - friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) >= rhs; } @@ -5128,11 +5128,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #endif } + // The friend comparisons with a scalar name the JSON type via decltype of + // their parameter in noexcept, because older MSVC versions do not see the + // class scope there: MSVC 2015 and 2017 take basic_json as the template, + // and MSVC 2019 16.0 rejects member types and template parameters. + /// @brief comparison: equal /// @sa https://json.nlohmann.me/api/basic_json/operator_eq/ template::value, int>::type = 0> - friend bool operator==(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator==(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs == basic_json(rhs); } @@ -5141,7 +5146,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_eq/ template::value, int>::type = 0> - friend bool operator==(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator==(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) == rhs; } @@ -5157,7 +5162,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ne/ template::value, int>::type = 0> - friend bool operator!=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator!=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs != basic_json(rhs); } @@ -5166,7 +5171,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ne/ template::value, int>::type = 0> - friend bool operator!=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator!=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) != rhs; } @@ -5186,7 +5191,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_lt/ template::value, int>::type = 0> - friend bool operator<(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs < basic_json(rhs); } @@ -5195,7 +5200,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_lt/ template::value, int>::type = 0> - friend bool operator<(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) < rhs; } @@ -5215,7 +5220,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_le/ template::value, int>::type = 0> - friend bool operator<=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs <= basic_json(rhs); } @@ -5224,7 +5229,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_le/ template::value, int>::type = 0> - friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) <= rhs; } @@ -5245,7 +5250,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_gt/ template::value, int>::type = 0> - friend bool operator>(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs > basic_json(rhs); } @@ -5254,7 +5259,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_gt/ template::value, int>::type = 0> - friend bool operator>(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) > rhs; } @@ -5274,7 +5279,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ge/ template::value, int>::type = 0> - friend bool operator>=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs >= basic_json(rhs); } @@ -5283,7 +5288,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ge/ template::value, int>::type = 0> - friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) >= rhs; } @@ -5319,6 +5324,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return o; } +#if !JSON_DELETE_DEPRECATED_FUNCTIONS + // the deleted version is a function template after the class, because + // GCC < 5 and Clang < 10 reject deleted friend functions in class templates /// @brief serialize to stream /// @sa https://json.nlohmann.me/api/operator_ltlt/ /// @deprecated This function is deprecated since 3.0.0 and will be removed in @@ -5327,9 +5335,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// replace calls like `j >> o;` with `o << j;`. JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator<<(std::ostream&, const basic_json&)) friend std::ostream& operator>>(const basic_json& j, std::ostream& o) -#if JSON_DELETE_DEPRECATED_FUNCTIONS - = delete; -#else { return o << j; } @@ -5507,6 +5512,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #pragma clang diagnostic pop #endif #ifndef JSON_NO_IO +#if !JSON_DELETE_DEPRECATED_FUNCTIONS + // the deleted version is a function template after the class, because + // GCC < 5 and Clang < 10 reject deleted friend functions in class templates /// @brief deserialize from stream /// @sa https://json.nlohmann.me/api/operator_gtgt/ /// @deprecated This stream operator is deprecated since 3.0.0 and will be removed in @@ -5515,9 +5523,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// replace calls like `j << i;` with `i >> j;`. JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator>>(std::istream&, basic_json&)) friend std::istream& operator<<(basic_json& j, std::istream& i) -#if JSON_DELETE_DEPRECATED_FUNCTIONS - = delete; -#else { return operator>>(i, j); } @@ -7178,6 +7183,18 @@ std::string format_as(const NLOHMANN_BASIC_JSON_TPL& j) return j.dump(); } +#if JSON_DELETE_DEPRECATED_FUNCTIONS && !defined(JSON_NO_IO) + /// @brief serialize to stream (deleted; use operator<<(std::ostream&, const basic_json&)) + /// @sa https://json.nlohmann.me/api/operator_ltlt/ + NLOHMANN_BASIC_JSON_TPL_DECLARATION + std::ostream& operator>>(const NLOHMANN_BASIC_JSON_TPL& j, std::ostream& o) = delete; + + /// @brief deserialize from stream (deleted; use operator>>(std::istream&, basic_json&)) + /// @sa https://json.nlohmann.me/api/operator_gtgt/ + NLOHMANN_BASIC_JSON_TPL_DECLARATION + std::istream& operator<<(NLOHMANN_BASIC_JSON_TPL& j, std::istream& i) = delete; +#endif + NLOHMANN_JSON_NAMESPACE_END /////////////////////// diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b427b247e..d0a651093 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -4905,6 +4905,21 @@ T conditional_static_cast(U value) return value; } +// like conditional_static_cast, but converts to bool by comparing with zero, +// because MSVC 2015 warns about any conversion to bool (C4800), even with an +// explicit cast; used for enums whose underlying type is bool +template < typename T, typename U, enable_if_t < !std::is_same::value, int > = 0 > +T bool_aware_static_cast(U value) +{ + return conditional_static_cast(value); +} + +template::value, int> = 0> +bool bool_aware_static_cast(U value) +{ + return value != U(); +} + template using all_integral = conjunction...>; @@ -5700,7 +5715,7 @@ inline void from_json(const BasicJsonType& j, EnumType& e) typename BasicJsonType::number_unsigned_t, underlying_type>::type; value_type val; get_arithmetic_value(j, val); - e = static_cast(static_cast(val)); + e = static_cast(bool_aware_static_cast(val)); } #endif // JSON_DISABLE_ENUM_SERIALIZATION @@ -7066,9 +7081,20 @@ struct external_constructor ///////////// #ifdef JSON_HAS_CPP_17 +// whether storing the value of a std::optional cannot throw; MSVC 2017 +// evaluates std::is_nothrow_assignable as true even if T's to_json throws, so +// the exception would call std::terminate (#5642) +#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER < 1920 + template + using is_nothrow_optional_to_json = std::false_type; +#else + template + using is_nothrow_optional_to_json = std::is_nothrow_assignable; +#endif + template::value, int> = 0> -void to_json(BasicJsonType& j, const std::optional& opt) noexcept(std::is_nothrow_assignable::value) +void to_json(BasicJsonType& j, const std::optional& opt) noexcept(is_nothrow_optional_to_json::value) { if (opt.has_value()) { @@ -7144,7 +7170,7 @@ inline void to_json(BasicJsonType& j, EnumType e) noexcept { using underlying_type = typename std::underlying_type::type; static constexpr value_t integral_value_t = std::is_unsigned::value ? value_t::number_unsigned : value_t::number_integer; - external_constructor::construct(j, static_cast(e)); + external_constructor::construct(j, bool_aware_static_cast(e)); } #endif // JSON_DISABLE_ENUM_SERIALIZATION @@ -14353,10 +14379,11 @@ class binary_reader } // the value is -1 - number, which fits into number_integer_t - // whenever number does + // whenever number does; the outer cast undoes the integral promotion + // for number_integer_t types narrower than int if (JSON_HEDLEY_LIKELY(value_in_range_of(number))) { - return sax->number_integer(static_cast(-1) - static_cast(number)); + return sax->number_integer(conditional_static_cast(static_cast(-1) - static_cast(number))); } // like the lexer does for JSON text, store a value too small for @@ -17694,7 +17721,8 @@ class binary_reader { return sax->number_unsigned(static_cast(number)); } - return emit_float(number); + // std::isfinite has no integer overloads in MSVC's + return emit_float(static_cast(number)); } /*! @@ -17717,7 +17745,8 @@ class binary_reader { return sax->number_unsigned(static_cast(number)); } - return emit_float(number); + // std::isfinite has no integer overloads in MSVC's + return emit_float(static_cast(number)); } /*! @@ -17728,7 +17757,8 @@ class binary_reader and NaN in the input are passed on unchanged. Integers only overflow if number_float_t cannot represent 2^64, e.g., a half-precision type. - @tparam NumberType a floating-point or integer type + @tparam NumberType a floating-point type (emit_signed and emit_unsigned + convert integers to long double first) @param[in] number the number @return whether the SAX parser accepted the value @@ -32248,7 +32278,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_le/ template requires std::is_scalar_v - friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) <= rhs; } @@ -32257,7 +32287,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ge/ template requires std::is_scalar_v - friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) >= rhs; } @@ -32278,11 +32308,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #endif } + // The friend comparisons with a scalar name the JSON type via decltype of + // their parameter in noexcept, because older MSVC versions do not see the + // class scope there: MSVC 2015 and 2017 take basic_json as the template, + // and MSVC 2019 16.0 rejects member types and template parameters. + /// @brief comparison: equal /// @sa https://json.nlohmann.me/api/basic_json/operator_eq/ template::value, int>::type = 0> - friend bool operator==(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator==(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs == basic_json(rhs); } @@ -32291,7 +32326,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_eq/ template::value, int>::type = 0> - friend bool operator==(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator==(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) == rhs; } @@ -32307,7 +32342,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ne/ template::value, int>::type = 0> - friend bool operator!=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator!=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs != basic_json(rhs); } @@ -32316,7 +32351,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ne/ template::value, int>::type = 0> - friend bool operator!=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator!=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) != rhs; } @@ -32336,7 +32371,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_lt/ template::value, int>::type = 0> - friend bool operator<(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs < basic_json(rhs); } @@ -32345,7 +32380,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_lt/ template::value, int>::type = 0> - friend bool operator<(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) < rhs; } @@ -32365,7 +32400,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_le/ template::value, int>::type = 0> - friend bool operator<=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs <= basic_json(rhs); } @@ -32374,7 +32409,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_le/ template::value, int>::type = 0> - friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) <= rhs; } @@ -32395,7 +32430,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_gt/ template::value, int>::type = 0> - friend bool operator>(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs > basic_json(rhs); } @@ -32404,7 +32439,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_gt/ template::value, int>::type = 0> - friend bool operator>(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) > rhs; } @@ -32424,7 +32459,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ge/ template::value, int>::type = 0> - friend bool operator>=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return lhs >= basic_json(rhs); } @@ -32433,7 +32468,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_ge/ template::value, int>::type = 0> - friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) + friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible, ScalarType>::value) { return basic_json(lhs) >= rhs; } @@ -32469,6 +32504,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return o; } +#if !JSON_DELETE_DEPRECATED_FUNCTIONS + // the deleted version is a function template after the class, because + // GCC < 5 and Clang < 10 reject deleted friend functions in class templates /// @brief serialize to stream /// @sa https://json.nlohmann.me/api/operator_ltlt/ /// @deprecated This function is deprecated since 3.0.0 and will be removed in @@ -32477,9 +32515,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// replace calls like `j >> o;` with `o << j;`. JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator<<(std::ostream&, const basic_json&)) friend std::ostream& operator>>(const basic_json& j, std::ostream& o) -#if JSON_DELETE_DEPRECATED_FUNCTIONS - = delete; -#else { return o << j; } @@ -32657,6 +32692,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #pragma clang diagnostic pop #endif #ifndef JSON_NO_IO +#if !JSON_DELETE_DEPRECATED_FUNCTIONS + // the deleted version is a function template after the class, because + // GCC < 5 and Clang < 10 reject deleted friend functions in class templates /// @brief deserialize from stream /// @sa https://json.nlohmann.me/api/operator_gtgt/ /// @deprecated This stream operator is deprecated since 3.0.0 and will be removed in @@ -32665,9 +32703,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// replace calls like `j << i;` with `i >> j;`. JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator>>(std::istream&, basic_json&)) friend std::istream& operator<<(basic_json& j, std::istream& i) -#if JSON_DELETE_DEPRECATED_FUNCTIONS - = delete; -#else { return operator>>(i, j); } @@ -34328,6 +34363,18 @@ std::string format_as(const NLOHMANN_BASIC_JSON_TPL& j) return j.dump(); } +#if JSON_DELETE_DEPRECATED_FUNCTIONS && !defined(JSON_NO_IO) + /// @brief serialize to stream (deleted; use operator<<(std::ostream&, const basic_json&)) + /// @sa https://json.nlohmann.me/api/operator_ltlt/ + NLOHMANN_BASIC_JSON_TPL_DECLARATION + std::ostream& operator>>(const NLOHMANN_BASIC_JSON_TPL& j, std::ostream& o) = delete; + + /// @brief deserialize from stream (deleted; use operator>>(std::istream&, basic_json&)) + /// @sa https://json.nlohmann.me/api/operator_gtgt/ + NLOHMANN_BASIC_JSON_TPL_DECLARATION + std::istream& operator<<(NLOHMANN_BASIC_JSON_TPL& j, std::istream& i) = delete; +#endif + NLOHMANN_JSON_NAMESPACE_END /////////////////////// diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aac6fee5e..8735934de 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ set(JSON_SIMDUTF_VERSION 9.1.0 CACHE STRING "The simdutf version used by JSON_Te set(JSON_32bitTest AUTO CACHE STRING "Enable the 32bit unit test (ON/OFF/AUTO/ONLY).") set(JSON_TestStandards "" CACHE STRING "The list of standards to test explicitly.") +set(JSON_TestShard "" CACHE STRING "Build only a part of the unit tests, given as / (e.g. 0/2), to split them across CI jobs with a time limit.") # using an env var, since this will also affect targets executing cmake (such as "ci_test_compiler_default") if (NOT "" STREQUAL "$ENV{JSON_FORCED_GLOBAL_COMPILE_OPTIONS}") @@ -288,6 +289,33 @@ elseif(NOT json_32bit_test) list(FILTER files EXCLUDE REGEX src/unit-32bit.cpp) endif() +# with JSON_TestShard=/, keep every -th unit test file, +# starting at (the glob is sorted, so the split is stable) +set(test_shard_index 0) +if(NOT "${JSON_TestShard}" STREQUAL "") + if(NOT JSON_TestShard MATCHES "^([0-9]+)/([1-9][0-9]*)$") + message(FATAL_ERROR "JSON_TestShard must be /, e.g. 0/2, not '${JSON_TestShard}'.") + endif() + set(test_shard_index ${CMAKE_MATCH_1}) + set(test_shard_count ${CMAKE_MATCH_2}) + if(NOT test_shard_index LESS test_shard_count) + message(FATAL_ERROR "JSON_TestShard: the index must be less than the count, not '${JSON_TestShard}'.") + endif() + list(LENGTH files test_file_count) + set(shard_files "") + set(file_position 0) + foreach(file ${files}) + math(EXPR file_shard "${file_position} % ${test_shard_count}") + if(file_shard EQUAL test_shard_index) + list(APPEND shard_files ${file}) + endif() + math(EXPR file_position "${file_position} + 1") + endforeach() + set(files ${shard_files}) + list(LENGTH files shard_file_count) + message(STATUS "Test shard ${JSON_TestShard}: ${shard_file_count} of ${test_file_count} unit test files") +endif() + foreach(file ${files}) json_test_add_test_for(${file} MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}) endforeach() @@ -305,6 +333,9 @@ if(json_32bit_test_only) return() endif() +# the following variants of single test files are only built in the first shard +if(test_shard_index EQUAL 0) + # test legacy comparison of discarded values json_test_set_test_options(test-comparison_legacy COMPILE_DEFINITIONS JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1 @@ -332,6 +363,8 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} ) +endif() + # *DO NOT* use json_test_set_test_options() below this line ############################################################################# diff --git a/tests/src/unit-allocator.cpp b/tests/src/unit-allocator.cpp index bc283ab28..a358e75f9 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -521,6 +521,10 @@ struct countdown_allocator : std::allocator TEST_CASE("converting a deeply nested value from another specialization fails cleanly (#5650)") { + // MSVC 2015's debug STL constructs the containers' debug proxies through + // the allocator in noexcept constructors, so a failing construction crashes + // the program there instead of throwing std::bad_alloc. Nothing to check. +#if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0) using countdown_json = nlohmann::basic_json 0); +#endif } namespace diff --git a/tests/src/unit-conversions2.cpp b/tests/src/unit-conversions2.cpp index eca482b83..c543e4ff8 100644 --- a/tests/src/unit-conversions2.cpp +++ b/tests/src/unit-conversions2.cpp @@ -191,8 +191,10 @@ TEST_CASE("value conversion") { enum class bool_enum : bool { off, on }; - CHECK(json(bool_enum::off).get() == bool_enum::off); - CHECK(json(bool_enum::on).get() == bool_enum::on); + // the extra parentheses keep doctest from printing the enum via its + // underlying type, which MSVC 2015 reports as C4800 + CHECK((json(bool_enum::off).get() == bool_enum::off)); + CHECK((json(bool_enum::on).get() == bool_enum::on)); } #endif @@ -851,8 +853,11 @@ TEST_CASE("std::optional") CHECK_THROWS_WITH_AS(json(opt), "cannot serialize throwing_to_json_type", std::runtime_error&); // the conversion is noexcept exactly when converting the contained value is + // (except with MSVC 2017, where it is never noexcept, see to_json.hpp) +#if !defined(_MSC_VER) || defined(__clang__) || _MSC_VER >= 1920 static_assert(!std::is_nothrow_constructible&>::value); static_assert(std::is_nothrow_constructible&>::value); +#endif } #endif } diff --git a/tests/src/unit-delete_deprecated_functions.cpp b/tests/src/unit-delete_deprecated_functions.cpp index f54e9966a..a1d895370 100644 --- a/tests/src/unit-delete_deprecated_functions.cpp +++ b/tests/src/unit-delete_deprecated_functions.cpp @@ -22,6 +22,7 @@ using nlohmann::json; #include #include #include +#include #include #include #include @@ -109,6 +110,10 @@ JSON_TEST_DETECT(ptr_at, std::declval().at(std::declval::value); } +#endif + SECTION("the non-deprecated functions still work") { const json j = {{"a", {1, 2}}}; @@ -210,5 +217,13 @@ TEST_CASE("JSON_DELETE_DEPRECATED_FUNCTIONS") CHECK(j.at(ptr) == 2); CHECK(j.value(ptr, 0) == 2); CHECK(j.contains(ptr)); + + std::ostringstream os; + os << j; + CHECK(os.str() == R"({"a":[1,2]})"); + std::istringstream is(os.str()); + json parsed; + is >> parsed; + CHECK(parsed == j); } } diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index 9a7a58e6b..1b59d8add 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -1551,6 +1551,10 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j // count(0) used to compile and then crash instead of failing to compile using nlohmann::detail::is_detected; + // MSVC 2015 does not treat selecting a deleted function in decltype as + // a substitution failure, so it detects the deleted overloads as + // callable; calling them still fails to compile +#if !(defined(_MSC_VER) && _MSC_VER < 1910) CHECK_FALSE(is_detected::value); CHECK_FALSE(is_detected::value); CHECK_FALSE(is_detected::value); @@ -1562,6 +1566,7 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j // another integral literal type must be rejected as well, not just int CHECK_FALSE(is_detected::value); +#endif // the valid overloads must remain callable CHECK(is_detected::value); From 5379e04ce4f820580afba6c4513f1424b511de76 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 6 Oct 2026 07:33:34 +0200 Subject: [PATCH 5/6] Throw type_error.321 when serializing discarded values to binary formats (#5761) * Throw type_error.321 when serializing discarded values to binary formats The CBOR, MessagePack, UBJSON, BJData, and BSON writers silently skipped the payload of a value_t::discarded value nested in an array or object, while still writing its slot in the element/member count (and, for BSON, its entry header), producing a binary document whose declared size does not match what was actually written. Throw type_error.321 instead, for a discarded value anywhere in the tree, including at the top level. Rewritten from the original PR against the current (non-recursive option aside) binary_writer.hpp, which has changed substantially since this was first proposed; the out_of_range.412 MessagePack size check and unrelated test reformatting from that PR are dropped as out of scope here. Signed-off-by: Niels Lohmann * Document and test type_error.321 for discarded binary values Add docs for the new exception (home/exceptions.md and the Exceptions sections of to_cbor/to_msgpack/to_ubjson/to_bjdata/to_bson) and test coverage for a discarded value nested in an array or object, nested deeper, and (for UBJSON/BJData) inside an optimized same-type array, for each of CBOR, MessagePack, UBJSON, BJData, and BSON. Adjust the three pre-existing "discarded" tests that asserted the old silent behavior (empty/short output) to expect type_error.321 instead. Co-authored-by: ameliabarnabyhub <312084480+ameliabarnabyhub@users.noreply.github.com> Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann Co-authored-by: ameliabarnabyhub Co-authored-by: ameliabarnabyhub <312084480+ameliabarnabyhub@users.noreply.github.com> --- docs/mkdocs/docs/api/basic_json/to_bjdata.md | 6 +- docs/mkdocs/docs/api/basic_json/to_bson.md | 5 ++ docs/mkdocs/docs/api/basic_json/to_cbor.md | 4 ++ docs/mkdocs/docs/api/basic_json/to_msgpack.md | 4 ++ docs/mkdocs/docs/api/basic_json/to_ubjson.md | 4 ++ docs/mkdocs/docs/home/exceptions.md | 14 ++++ .../nlohmann/detail/output/binary_writer.hpp | 30 +++++++-- single_include/nlohmann/json.hpp | 30 +++++++-- tests/src/unit-bjdata.cpp | 55 +++++++++++++++- tests/src/unit-bson.cpp | 48 ++++++++++++++ tests/src/unit-cbor.cpp | 45 ++++++++++++- tests/src/unit-msgpack.cpp | 45 ++++++++++++- tests/src/unit-ubjson.cpp | 64 +++++++++++++++++-- 13 files changed, 329 insertions(+), 25 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/to_bjdata.md b/docs/mkdocs/docs/api/basic_json/to_bjdata.md index df3b69004..583139068 100644 --- a/docs/mkdocs/docs/api/basic_json/to_bjdata.md +++ b/docs/mkdocs/docs/api/basic_json/to_bjdata.md @@ -68,6 +68,8 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key in `j` is not valid UTF-8 and `error_handler` is `strict` (the default only if [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) +- Throws [type_error.321](../../home/exceptions.md#jsonexceptiontype_error321) if `j` or a value nested in it is + discarded; example: `"cannot serialize discarded value to BJData"` ## Complexity @@ -119,4 +121,6 @@ Linear in the size of the JSON value `j`. - BJData version parameter (for draft3 binary encoding) added in version 3.12.0. - Added `error_handler` parameter in version 3.13.0. Its default, `keep`, writes the bytes of a string or object key that is not valid UTF-8 unchanged, as before; `strict` (the default if - [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) throws `type_error.316`. \ No newline at end of file + [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) throws `type_error.316`. +- Throws `type_error.321` for a discarded value since version 3.13.0; previously, a discarded value nested in an + array or object was silently skipped, producing invalid BJData. \ No newline at end of file diff --git a/docs/mkdocs/docs/api/basic_json/to_bson.md b/docs/mkdocs/docs/api/basic_json/to_bson.md index ad974e048..05f8b8548 100644 --- a/docs/mkdocs/docs/api/basic_json/to_bson.md +++ b/docs/mkdocs/docs/api/basic_json/to_bson.md @@ -58,6 +58,9 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key is not valid UTF-8 and `error_handler` is `strict` (the default only if [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) +- Throws [type_error.321](../../home/exceptions.md#jsonexceptiontype_error321) if a value nested in `j` is discarded + (the top-level value itself is covered by `type_error.317` above, since it must be an object); example: + `"cannot serialize discarded value to BSON"` ## Complexity @@ -110,6 +113,8 @@ pass before anything is written. - Throws `out_of_range.412` and `out_of_range.415` since version 3.13.0. - Linear in the size of `j`, and no longer limited by the call stack for deeply nested values, since version 3.13.0. - `out_of_range.415` is now detected before anything is written, like the other exceptions above, since version 3.13.0. +- Throws `type_error.321` for a discarded value nested in `j` since version 3.13.0; previously, it was silently + skipped, producing a document whose declared size did not match what was actually written. - Added `error_handler` parameter in version 3.13.0. Its default, `keep`, writes the bytes of a string or object key that is not valid UTF-8 unchanged, as before; `strict` (the default if [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) throws `type_error.316` before anything diff --git a/docs/mkdocs/docs/api/basic_json/to_cbor.md b/docs/mkdocs/docs/api/basic_json/to_cbor.md index c72f310bb..f5b6a07bb 100644 --- a/docs/mkdocs/docs/api/basic_json/to_cbor.md +++ b/docs/mkdocs/docs/api/basic_json/to_cbor.md @@ -49,6 +49,8 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key in `j` is not valid UTF-8 and `error_handler` is `strict` (the default only if [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) +- Throws [type_error.321](../../home/exceptions.md#jsonexceptiontype_error321) if `j` or a value nested in it is + discarded; example: `"cannot serialize discarded value to CBOR"` ## Complexity @@ -86,3 +88,5 @@ Linear in the size of the JSON value `j`. - Added `error_handler` parameter in version 3.13.0. Its default, `keep`, writes the bytes of a string or object key that is not valid UTF-8 unchanged, as before; `strict` (the default if [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) throws `type_error.316`. +- Throws `type_error.321` for a discarded value since version 3.13.0; previously, a discarded value nested in an + array or object was silently skipped, producing invalid CBOR. diff --git a/docs/mkdocs/docs/api/basic_json/to_msgpack.md b/docs/mkdocs/docs/api/basic_json/to_msgpack.md index 17b9fe29c..2e32208bf 100644 --- a/docs/mkdocs/docs/api/basic_json/to_msgpack.md +++ b/docs/mkdocs/docs/api/basic_json/to_msgpack.md @@ -54,6 +54,8 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va `"subtype 70000 is too large for the MessagePack ext type (max 255)"` - Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key in `j` is not valid UTF-8 and `error_handler` is `strict` +- Throws [type_error.321](../../home/exceptions.md#jsonexceptiontype_error321) if `j` or a value nested in it is + discarded; example: `"cannot serialize discarded value to MessagePack"` ## Complexity @@ -108,3 +110,5 @@ Linear in the size of the JSON value `j`. - Fixed in version 3.13.0 to serialize `number_integer_t`/`number_unsigned_t` pairs of different width correctly; before, integers could be serialized with the wrong value if `number_integer_t` was narrower than `number_unsigned_t`. +- Throws `type_error.321` for a discarded value since version 3.13.0; previously, a discarded value nested in an + array or object was silently skipped, producing invalid MessagePack. diff --git a/docs/mkdocs/docs/api/basic_json/to_ubjson.md b/docs/mkdocs/docs/api/basic_json/to_ubjson.md index 6860c01c6..6aad86683 100644 --- a/docs/mkdocs/docs/api/basic_json/to_ubjson.md +++ b/docs/mkdocs/docs/api/basic_json/to_ubjson.md @@ -61,6 +61,8 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va - Throws [type_error.316](../../home/exceptions.md#jsonexceptiontype_error316) if a string or object key in `j` is not valid UTF-8 and `error_handler` is `strict` (the default only if [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) +- Throws [type_error.321](../../home/exceptions.md#jsonexceptiontype_error321) if `j` or a value nested in it is + discarded; example: `"cannot serialize discarded value to UBJSON"` ## Complexity @@ -112,3 +114,5 @@ Linear in the size of the JSON value `j`. - Added `error_handler` parameter in version 3.13.0. Its default, `keep`, writes the bytes of a string or object key that is not valid UTF-8 unchanged, as before; `strict` (the default if [`JSON_STRICT_BINARY_UTF8`](../macros/json_strict_binary_utf8.md) is enabled) throws `type_error.316`. +- Throws `type_error.321` for a discarded value since version 3.13.0; previously, a discarded value nested in an + array or object was silently skipped, producing invalid UBJSON. diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 673c0c8dd..9cb5c8f62 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -804,6 +804,20 @@ does not list an enumerator and it is therefore converted like the first listed [json.exception.type_error.318] duplicate object key 'red' ``` +### json.exception.type_error.321 + +A discarded value (one created by [`parse()`](../api/basic_json/parse.md) with a callback that returns `false` for the +value, or by default-constructing a [`basic_json`](../api/basic_json/index.md) with +[`value_t::discarded`](../api/basic_json/value_t.md)) was passed to a binary serialization function, either directly or +nested in an array or object. There is no way to represent a discarded value in CBOR, MessagePack, UBJSON, BJData, or BSON. + +!!! failure "Example message" + + Serializing `#!json [1, 2]` to CBOR, where the second element was discarded by a parser callback: + ``` + [json.exception.type_error.321] cannot serialize discarded value to CBOR + ``` + ## Out of range This exception is thrown in case a library function is called on an input parameter that exceeds the expected range, for instance, in the case of array indices or nonexisting object keys. diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index a4921f217..8a26a451c 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -127,6 +127,7 @@ class binary_writer @throw type_error.316 if a string value or an object key is not valid UTF-8 @throw type_error.317 if @a j is not an object + @throw type_error.321 if a value nested in @a j is discarded */ void write_bson(const BasicJsonType& j) { @@ -158,6 +159,7 @@ class binary_writer @param[in] j JSON value to serialize @throw type_error.316 if a string value or an object key is not valid UTF-8 + @throw type_error.321 if @a j or a value nested in it is discarded */ void write_cbor(const BasicJsonType& j) { @@ -322,7 +324,7 @@ class binary_writer case value_t::discarded: default: - break; + throw_on_discarded(j, "CBOR"); } } @@ -382,6 +384,7 @@ class binary_writer /*! @param[in] j JSON value to serialize + @throw type_error.321 if @a j or a value nested in it is discarded */ void write_msgpack(const BasicJsonType& j) { @@ -655,7 +658,7 @@ class binary_writer case value_t::discarded: default: - break; + throw_on_discarded(j, "MessagePack"); } } @@ -668,6 +671,7 @@ class binary_writer @param[in] bjdata_version which BJData version to use, default is draft2 @throw type_error.316 if a string value or an object key is not valid UTF-8 + @throw type_error.321 if @a j or a value nested in it is discarded */ void write_ubjson(const BasicJsonType& j, const bool use_count, const bool use_type, const bool add_prefix = true, @@ -901,7 +905,7 @@ class binary_writer case value_t::discarded: default: - break; + throw_on_discarded(j, use_bjdata ? "BJData" : "UBJSON"); } } @@ -921,6 +925,15 @@ class binary_writer } private: + /*! + @brief throws because @a j is discarded and cannot be serialized + @throw type_error.321 always + */ + JSON_HEDLEY_NO_RETURN static void throw_on_discarded(const BasicJsonType& j, const char* format_name) + { + JSON_THROW(type_error::create(321, concat("cannot serialize discarded value to ", format_name), &j)); + } + ////////// // BSON // ////////// @@ -1172,6 +1185,7 @@ class binary_writer into a byte, before anything is written @throw type_error.316 if @a j is a string that is not valid UTF-8, before anything is written + @throw type_error.321 if @a j is discarded */ std::size_t calc_bson_value_size(const BasicJsonType& j) { @@ -1198,10 +1212,12 @@ class binary_writer case value_t::null: return 0ul; + case value_t::discarded: + throw_on_discarded(j, "BSON"); + // LCOV_EXCL_START case value_t::object: case value_t::array: - case value_t::discarded: default: JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) return 0ul; @@ -1238,10 +1254,12 @@ class binary_writer case value_t::null: return write_bson_null(name); + case value_t::discarded: + throw_on_discarded(j, "BSON"); + // LCOV_EXCL_START case value_t::object: case value_t::array: - case value_t::discarded: default: JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) return; @@ -1308,6 +1326,8 @@ class binary_writer byte, before anything is written @throw type_error.316 if a string value or a key is not valid UTF-8, before anything is written + @throw type_error.321 if a value nested in @a document is discarded, + before anything is written */ std::size_t calc_bson_sizes(const BasicJsonType& document, std::vector& nested_sizes) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index d0a651093..3c7b9704d 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21587,6 +21587,7 @@ class binary_writer @throw type_error.316 if a string value or an object key is not valid UTF-8 @throw type_error.317 if @a j is not an object + @throw type_error.321 if a value nested in @a j is discarded */ void write_bson(const BasicJsonType& j) { @@ -21618,6 +21619,7 @@ class binary_writer @param[in] j JSON value to serialize @throw type_error.316 if a string value or an object key is not valid UTF-8 + @throw type_error.321 if @a j or a value nested in it is discarded */ void write_cbor(const BasicJsonType& j) { @@ -21782,7 +21784,7 @@ class binary_writer case value_t::discarded: default: - break; + throw_on_discarded(j, "CBOR"); } } @@ -21842,6 +21844,7 @@ class binary_writer /*! @param[in] j JSON value to serialize + @throw type_error.321 if @a j or a value nested in it is discarded */ void write_msgpack(const BasicJsonType& j) { @@ -22115,7 +22118,7 @@ class binary_writer case value_t::discarded: default: - break; + throw_on_discarded(j, "MessagePack"); } } @@ -22128,6 +22131,7 @@ class binary_writer @param[in] bjdata_version which BJData version to use, default is draft2 @throw type_error.316 if a string value or an object key is not valid UTF-8 + @throw type_error.321 if @a j or a value nested in it is discarded */ void write_ubjson(const BasicJsonType& j, const bool use_count, const bool use_type, const bool add_prefix = true, @@ -22361,7 +22365,7 @@ class binary_writer case value_t::discarded: default: - break; + throw_on_discarded(j, use_bjdata ? "BJData" : "UBJSON"); } } @@ -22381,6 +22385,15 @@ class binary_writer } private: + /*! + @brief throws because @a j is discarded and cannot be serialized + @throw type_error.321 always + */ + JSON_HEDLEY_NO_RETURN static void throw_on_discarded(const BasicJsonType& j, const char* format_name) + { + JSON_THROW(type_error::create(321, concat("cannot serialize discarded value to ", format_name), &j)); + } + ////////// // BSON // ////////// @@ -22632,6 +22645,7 @@ class binary_writer into a byte, before anything is written @throw type_error.316 if @a j is a string that is not valid UTF-8, before anything is written + @throw type_error.321 if @a j is discarded */ std::size_t calc_bson_value_size(const BasicJsonType& j) { @@ -22658,10 +22672,12 @@ class binary_writer case value_t::null: return 0ul; + case value_t::discarded: + throw_on_discarded(j, "BSON"); + // LCOV_EXCL_START case value_t::object: case value_t::array: - case value_t::discarded: default: JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) return 0ul; @@ -22698,10 +22714,12 @@ class binary_writer case value_t::null: return write_bson_null(name); + case value_t::discarded: + throw_on_discarded(j, "BSON"); + // LCOV_EXCL_START case value_t::object: case value_t::array: - case value_t::discarded: default: JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) return; @@ -22768,6 +22786,8 @@ class binary_writer byte, before anything is written @throw type_error.316 if a string value or a key is not valid UTF-8, before anything is written + @throw type_error.321 if a value nested in @a document is discarded, + before anything is written */ std::size_t calc_bson_sizes(const BasicJsonType& document, std::vector& nested_sizes) { diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 5e703a357..cd989ef88 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -114,10 +114,59 @@ TEST_CASE("BJData") { SECTION("discarded") { - // discarded values are not serialized + // a discarded value cannot be serialized to BJData json const j = json::value_t::discarded; - const auto result = json::to_bjdata(j); - CHECK(result.empty()); + CHECK_THROWS_WITH_AS(json::to_bjdata(j), "[json.exception.type_error.321] cannot serialize discarded value to BJData", json::type_error&); + } + + SECTION("discarded values nested in a container") + { + json const discarded = json::value_t::discarded; + + SECTION("in an array") + { + json const j = {1, discarded, 2}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bjdata(j), "[json.exception.type_error.321] (/1) cannot serialize discarded value to BJData", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bjdata(j), "[json.exception.type_error.321] cannot serialize discarded value to BJData", json::type_error&); +#endif + } + + SECTION("as an object value") + { + json j; + j["a"] = 1; + j["b"] = discarded; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bjdata(j), "[json.exception.type_error.321] (/b) cannot serialize discarded value to BJData", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bjdata(j), "[json.exception.type_error.321] cannot serialize discarded value to BJData", json::type_error&); +#endif + } + + SECTION("nested deeper (array in object in array)") + { + json inner_array = {1, discarded}; + json middle_object; + middle_object["x"] = inner_array; + json const j = {middle_object}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bjdata(j), "[json.exception.type_error.321] (/0/x/1) cannot serialize discarded value to BJData", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bjdata(j), "[json.exception.type_error.321] cannot serialize discarded value to BJData", json::type_error&); +#endif + } + + SECTION("optimized array of all-discarded elements") + { + json const j = {discarded, discarded}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bjdata(j, true, true), "[json.exception.type_error.321] (/0) cannot serialize discarded value to BJData", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bjdata(j, true, true), "[json.exception.type_error.321] cannot serialize discarded value to BJData", json::type_error&); +#endif + } } SECTION("null") diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index c9c96b693..352ad6f1a 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -149,6 +149,54 @@ TEST_CASE("BSON") json const j = std::vector {1, 2, 3, 4, 5, 6, 7}; CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is array", json::type_error&); } + + SECTION("discarded") + { + json const j = json::value_t::discarded; + CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.317] to serialize to BSON, top-level type must be object, but is discarded", json::type_error&); + } + } + + SECTION("discarded values nested in a container cannot be serialized to BSON") + { + json const discarded = json::value_t::discarded; + + SECTION("as an object value") + { + json j; + j["a"] = 1; + j["b"] = discarded; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] (/b) cannot serialize discarded value to BSON", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] cannot serialize discarded value to BSON", json::type_error&); +#endif + } + + SECTION("in an array that is an object value") + { + json j; + j["a"] = json::array({1, discarded, 2}); +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] (/a/1) cannot serialize discarded value to BSON", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] cannot serialize discarded value to BSON", json::type_error&); +#endif + } + + SECTION("nested deeper (array in object in object)") + { + json inner_array = {1, discarded}; + json middle_object; + middle_object["x"] = inner_array; + json j; + j["outer"] = middle_object; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] (/outer/x/1) cannot serialize discarded value to BSON", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_bson(j), "[json.exception.type_error.321] cannot serialize discarded value to BSON", json::type_error&); +#endif + } } SECTION("keys containing code-point U+0000 cannot be serialized to BSON") diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 1f584d3b3..38763b4db 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -38,10 +38,49 @@ TEST_CASE("CBOR") { SECTION("discarded") { - // discarded values are not serialized + // a discarded value cannot be serialized to CBOR json const j = json::value_t::discarded; - const auto result = json::to_cbor(j); - CHECK(result.empty()); + CHECK_THROWS_WITH_AS(json::to_cbor(j), "[json.exception.type_error.321] cannot serialize discarded value to CBOR", json::type_error&); + } + + SECTION("discarded values nested in a container") + { + json const discarded = json::value_t::discarded; + + SECTION("in an array") + { + json const j = {1, discarded, 2}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_cbor(j), "[json.exception.type_error.321] (/1) cannot serialize discarded value to CBOR", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_cbor(j), "[json.exception.type_error.321] cannot serialize discarded value to CBOR", json::type_error&); +#endif + } + + SECTION("as an object value") + { + json j; + j["a"] = 1; + j["b"] = discarded; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_cbor(j), "[json.exception.type_error.321] (/b) cannot serialize discarded value to CBOR", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_cbor(j), "[json.exception.type_error.321] cannot serialize discarded value to CBOR", json::type_error&); +#endif + } + + SECTION("nested deeper (array in object in array)") + { + json inner_array = {1, discarded}; + json middle_object; + middle_object["x"] = inner_array; + json const j = {middle_object}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_cbor(j), "[json.exception.type_error.321] (/0/x/1) cannot serialize discarded value to CBOR", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_cbor(j), "[json.exception.type_error.321] cannot serialize discarded value to CBOR", json::type_error&); +#endif + } } SECTION("NaN") diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index d0db8438c..bc29b301b 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -41,10 +41,49 @@ TEST_CASE("MessagePack") { SECTION("discarded") { - // discarded values are not serialized + // a discarded value cannot be serialized to MessagePack json const j = json::value_t::discarded; - const auto result = json::to_msgpack(j); - CHECK(result.empty()); + CHECK_THROWS_WITH_AS(json::to_msgpack(j), "[json.exception.type_error.321] cannot serialize discarded value to MessagePack", json::type_error&); + } + + SECTION("discarded values nested in a container") + { + json const discarded = json::value_t::discarded; + + SECTION("in an array") + { + json const j = {1, discarded, 2}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_msgpack(j), "[json.exception.type_error.321] (/1) cannot serialize discarded value to MessagePack", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_msgpack(j), "[json.exception.type_error.321] cannot serialize discarded value to MessagePack", json::type_error&); +#endif + } + + SECTION("as an object value") + { + json j; + j["a"] = 1; + j["b"] = discarded; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_msgpack(j), "[json.exception.type_error.321] (/b) cannot serialize discarded value to MessagePack", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_msgpack(j), "[json.exception.type_error.321] cannot serialize discarded value to MessagePack", json::type_error&); +#endif + } + + SECTION("nested deeper (array in object in array)") + { + json inner_array = {1, discarded}; + json middle_object; + middle_object["x"] = inner_array; + json const j = {middle_object}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_msgpack(j), "[json.exception.type_error.321] (/0/x/1) cannot serialize discarded value to MessagePack", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_msgpack(j), "[json.exception.type_error.321] cannot serialize discarded value to MessagePack", json::type_error&); +#endif + } } SECTION("null") diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index a4060bd53..9a45546b6 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -35,10 +35,59 @@ TEST_CASE("UBJSON") { SECTION("discarded") { - // discarded values are not serialized + // a discarded value cannot be serialized to UBJSON json const j = json::value_t::discarded; - const auto result = json::to_ubjson(j); - CHECK(result.empty()); + CHECK_THROWS_WITH_AS(json::to_ubjson(j), "[json.exception.type_error.321] cannot serialize discarded value to UBJSON", json::type_error&); + } + + SECTION("discarded values nested in a container") + { + json const discarded = json::value_t::discarded; + + SECTION("in an array") + { + json const j = {1, discarded, 2}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_ubjson(j), "[json.exception.type_error.321] (/1) cannot serialize discarded value to UBJSON", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_ubjson(j), "[json.exception.type_error.321] cannot serialize discarded value to UBJSON", json::type_error&); +#endif + } + + SECTION("as an object value") + { + json j; + j["a"] = 1; + j["b"] = discarded; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_ubjson(j), "[json.exception.type_error.321] (/b) cannot serialize discarded value to UBJSON", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_ubjson(j), "[json.exception.type_error.321] cannot serialize discarded value to UBJSON", json::type_error&); +#endif + } + + SECTION("nested deeper (array in object in array)") + { + json inner_array = {1, discarded}; + json middle_object; + middle_object["x"] = inner_array; + json const j = {middle_object}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_ubjson(j), "[json.exception.type_error.321] (/0/x/1) cannot serialize discarded value to UBJSON", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_ubjson(j), "[json.exception.type_error.321] cannot serialize discarded value to UBJSON", json::type_error&); +#endif + } + + SECTION("optimized array of all-discarded elements") + { + json const j = {discarded, discarded}; +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_ubjson(j, true, true), "[json.exception.type_error.321] (/0) cannot serialize discarded value to UBJSON", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_ubjson(j, true, true), "[json.exception.type_error.321] cannot serialize discarded value to UBJSON", json::type_error&); +#endif + } } SECTION("null") @@ -2099,9 +2148,14 @@ TEST_CASE("UBJSON") SECTION("discarded") { + // a discarded value cannot be serialized to UBJSON, even as part + // of an optimized array of a single (here: valueless) type json const j = {json::value_t::discarded, json::value_t::discarded}; - std::vector expected = {'[', '$', 'N', '#', 'i', 2}; - CHECK(json::to_ubjson(j, true, true) == expected); +#if JSON_DIAGNOSTICS + CHECK_THROWS_WITH_AS(json::to_ubjson(j, true, true), "[json.exception.type_error.321] (/0) cannot serialize discarded value to UBJSON", json::type_error&); +#else + CHECK_THROWS_WITH_AS(json::to_ubjson(j, true, true), "[json.exception.type_error.321] cannot serialize discarded value to UBJSON", json::type_error&); +#endif } } } From 0a365865f9338ee7a35b3f7d22ed7f808b75385d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 6 Oct 2026 07:40:39 +0200 Subject: [PATCH 6/6] Make basic_json destruction allocation-free and non-recursive (#5762) * Use the provided allocator in destroy() (#4842) Uses the provided allocator to allocate the stack used to avoid recursion in the destroy() implementation used by ~basic_json. Signed-off-by: Niels Lohmann * Test that the destructor uses the provided allocator Adds a regression test for #4842: destroying a nested array or object must allocate its temporary stack through the basic_json allocator, not std::allocator. Signed-off-by: Niels Lohmann * Fix allocation failure during JSON destruction Signed-off-by: Michael Sam Signed-off-by: Niels Lohmann * Make json_value::destroy() non-recursive and allocation-free destroy() used to flatten a nested array/object into a heap-allocated std::vector to avoid recursing per nesting level. That vector could itself throw bad_alloc under memory pressure, and since it now used the basic_json's own allocator (#4842), a failing allocator supplied by the caller made this more likely, not less. An exception thrown from inside ~basic_json(), which is noexcept, terminates the program (#5135). Replace the vector-based stack with a pointer-reversal walk that visits the tree without recursing per level and without allocating anything: cur is the array/object currently being emptied, prev is its parent (or null at the top). A parent's last child slot doubles as storage for that parent's own parent link while we are below it, so no extra memory is needed. A child is only ever removed once it is a scalar or an empty array/object, which neither allocates nor recurses more than one level deep. take() moves m_data between these locals directly, bypassing set_parents()/assert_invariant() (the former is O(#children) per call under JSON_DIAGNOSTICS, which would make the walk quadratic otherwise). This also removes the std::vector stack added by #4842, so the extra allocations it introduced disappear along with it. Co-authored-by: Michael Sam <9461037+michaelsam94@users.noreply.github.com> Signed-off-by: Niels Lohmann * Test that destroy() performs no allocation, even under memory pressure Update the #4842 regression test: it used to check that destroying a nested array/object made at least one allocation through the provided allocator (the old flattening stack). Now that destroy() does not allocate at all, assert the opposite: zero allocations, deallocations only. Rework the #5135 regression test to use a dedicated failing/counting allocator instead of overriding the process-wide ::operator new and ::operator delete, which affected every allocation in the whole unit-regression2 binary rather than just the values under test. Keep the original small repro as one case, and add deep (100000 levels) and wide-and-deep nested array/object/ordered_json cases, all destroyed while every further allocation is made to fail: the destructor must complete without allocating, without throwing, and without leaking. Co-authored-by: Michael Sam <9461037+michaelsam94@users.noreply.github.com> Signed-off-by: Niels Lohmann * Refactor destroy() for readability and add edge-case tests Apply review feedback from Greg Marr on the json_value::destroy() non-recursive, allocation-free destruction walk (#5135): - last_child() now uses object->rbegin()->second instead of std::prev(object->end())->second; pop_last_child() keeps std::prev(end()) since erase() needs a forward iterator. - is_empty_container() becomes has_no_children(), a switch that returns true for every non-container type as well as empty array/object, simplifying the "scalar or already-empty child" check at the call site. The local variable `last` is renamed to `cur_last_ref` for clarity. - free_container() asserts the array/object is already empty before freeing it, and the object branches assert the expected type. - destroy(value_t t) is now a thin dispatcher to destroy_string(), destroy_binary(), and destroy_container(t), each handling its own "not initialized" check and sharing the simple cases first in the switch. - destroy_container() moves the top-level container into the local stand-in via a plain swap of the json_value union, instead of a manual copy plus clearing array/object by hand. - The "cur has no children and there is no parent" case now frees cur and returns immediately, so the main loop is a plain while (true) with no trailing code after it. Also adds edge-case tests for both json and ordered_json (mixes of empty/non-empty arrays and objects, container children in first/last position, single-element chains, top-level empty containers, and destruction via erase()/assignment), plus a mixed-tree case in the "destructor performs no allocation" test. Signed-off-by: Niels Lohmann * Make the destroy() walk helpers private They modify basic_json internals without maintaining its invariants and are only meant for destroy_container(), so they no longer need to be reachable from the rest of basic_json. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann Signed-off-by: Michael Sam Co-authored-by: Vesko Karaganev Co-authored-by: Michael Sam Co-authored-by: Michael Sam <9461037+michaelsam94@users.noreply.github.com> --- include/nlohmann/json.hpp | 254 +++++++++++++++++++-------- single_include/nlohmann/json.hpp | 254 +++++++++++++++++++-------- tests/src/unit-allocator.cpp | 85 +++++++++ tests/src/unit-regression2.cpp | 287 +++++++++++++++++++++++++++++++ 4 files changed, 734 insertions(+), 146 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 6f6bb8d04..aa8b1a6c6 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -612,100 +612,210 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// constructor for rvalue binary arrays (internal type) json_value(binary_t&& value) : binary(create(std::move(value))) {} - void destroy(value_t t) +private: + // raw, allocation-free transfer of m_data from src to dst: no + // set_parents()/assert_invariant() (the former is O(#children) per + // call under JSON_DIAGNOSTICS, which would make the walk below + // quadratic); dst takes ownership, src is left as value_t::null. + static void take(basic_json& dst, basic_json& src) noexcept + { + dst.m_data.m_type = src.m_data.m_type; + dst.m_data.m_value = src.m_data.m_value; + src.m_data.m_type = value_t::null; + } + + // true if v is not an array/object, or is an already-empty one + static bool has_no_children(const basic_json& v) noexcept + { + switch (v.m_data.m_type) + { + case value_t::array: + return v.m_data.m_value.array->empty(); + case value_t::object: + return v.m_data.m_value.object->empty(); + default: + return true; + } + } + + static basic_json& last_child(basic_json& v) + { + if (v.m_data.m_type == value_t::array) + { + return v.m_data.m_value.array->back(); + } + JSON_ASSERT(v.m_data.m_type == value_t::object); + return v.m_data.m_value.object->rbegin()->second; + } + + // removes the last child of a non-empty array/object v; this never + // allocates, and since it is only ever called when that child is a + // scalar or an already-empty array/object, destroying it never + // recurses more than one level deep (see destroy() below) + static void pop_last_child(basic_json& v) + { + if (v.m_data.m_type == value_t::array) + { + v.m_data.m_value.array->pop_back(); + } + else + { + JSON_ASSERT(v.m_data.m_type == value_t::object); + // erase() needs a forward iterator, so std::prev(end()) is + // used here rather than rbegin() (see last_child() above) + v.m_data.m_value.object->erase(std::prev(v.m_data.m_value.object->end())); + } + } + + // deallocates the (already empty) array/object held by v; this is + // the same allocator-based free the old recursive implementation + // used, just factored out so every level of the walk in destroy() + // can share it + static void free_container(basic_json& v) noexcept + { + if (v.m_data.m_type == value_t::array) + { + JSON_ASSERT(v.m_data.m_value.array->empty()); + AllocatorType alloc; + std::allocator_traits::destroy(alloc, v.m_data.m_value.array); + std::allocator_traits::deallocate(alloc, v.m_data.m_value.array, 1); + } + else + { + JSON_ASSERT(v.m_data.m_type == value_t::object); + JSON_ASSERT(v.m_data.m_value.object->empty()); + AllocatorType alloc; + std::allocator_traits::destroy(alloc, v.m_data.m_value.object); + std::allocator_traits::deallocate(alloc, v.m_data.m_value.object, 1); + } + v.m_data.m_type = value_t::null; // avoid a double free if v is later destructed + } + +public: + void destroy_string() noexcept + { + if (string == nullptr) + { + // not initialized (e.g., due to exception in the ctor) + return; + } + AllocatorType alloc; + std::allocator_traits::destroy(alloc, string); + std::allocator_traits::deallocate(alloc, string, 1); + } + + void destroy_binary() noexcept + { + if (binary == nullptr) + { + // not initialized (e.g., due to exception in the ctor) + return; + } + AllocatorType alloc; + std::allocator_traits::destroy(alloc, binary); + std::allocator_traits::deallocate(alloc, binary, 1); + } + + // t must be value_t::array or value_t::object + void destroy_container(value_t t) noexcept { if ( (t == value_t::object && object == nullptr) || - (t == value_t::array && array == nullptr) || - (t == value_t::string && string == nullptr) || - (t == value_t::binary && binary == nullptr) + (t == value_t::array && array == nullptr) ) { // not initialized (e.g., due to exception in the ctor) return; } - if (t == value_t::array || t == value_t::object) + + // Destroy the tree without recursing per nesting level and + // without any heap allocation: a heap-allocated flattening + // stack (the previous implementation) can itself throw + // bad_alloc, which would escape this noexcept destructor and + // terminate the program (#5135). + // + // Instead, walk down the "last child" chain, reversing links + // as we go: cur is the container currently being emptied, + // and prev is its parent (value_t::null when there is none). + // Each parent's last child slot doubles as storage for that + // parent's own parent link while we are below it, so no + // extra memory is needed. We only ever remove a child once + // it is a scalar or an empty array/object, which neither + // allocates nor recurses more than one level deep. + // + // This json_value is not itself a basic_json, so the + // top-level container is first moved into a local stand-in + // ("cur"); a default-constructed basic_json has a null + // pointer in its m_value (see data::m_value's initializer), + // so swapping it with *this leaves this union's own pointer + // null, and it is never looked at or freed a second time. + basic_json cur; + cur.m_data.m_type = t; + using std::swap; + swap(cur.m_data.m_value, *this); + + basic_json prev; // value_t::null: no parent + + while (true) { - // flatten the current json_value to a heap-allocated stack - std::vector stack; - - // move the top-level items to stack - if (t == value_t::array) + if (has_no_children(cur)) { - stack.reserve(array->size()); - std::move(array->begin(), array->end(), std::back_inserter(stack)); - } - else - { - stack.reserve(object->size()); - for (auto&& it : *object) + if (prev.m_data.m_type == value_t::null) { - stack.push_back(std::move(it.second)); - } - } - - while (!stack.empty()) - { - // move the last item to a local variable to be processed - basic_json current_item(std::move(stack.back())); - stack.pop_back(); - - // if current_item is array/object, move - // its children to the stack to be processed later - if (current_item.is_array()) - { - std::move(current_item.m_data.m_value.array->begin(), current_item.m_data.m_value.array->end(), std::back_inserter(stack)); - - current_item.m_data.m_value.array->clear(); - } - else if (current_item.is_object()) - { - for (auto&& it : *current_item.m_data.m_value.object) - { - stack.push_back(std::move(it.second)); - } - - current_item.m_data.m_value.object->clear(); + free_container(cur); + return; // back at the top with nothing left to do } - // it's now safe that current_item gets destructed - // since it doesn't have any children + // ascend: detach the grandparent link from prev's + // last slot, drop that (now null) slot, free cur + // (it is empty), then move up one level + basic_json gp; + take(gp, last_child(prev)); + pop_last_child(prev); + + free_container(cur); + + take(cur, prev); + take(prev, gp); + continue; } + + basic_json& cur_last_ref = last_child(cur); + + if (has_no_children(cur_last_ref)) + { + // scalar, or already-empty array/object + pop_last_child(cur); + continue; + } + + // descend into the non-empty last child, reversing the + // link: its slot takes over prev, and the child becomes + // the new cur + basic_json tmp; + take(tmp, cur_last_ref); + take(cur_last_ref, prev); + take(prev, cur); + take(cur, tmp); } + } + void destroy(value_t t) + { switch (t) { - case value_t::object: - { - AllocatorType alloc; - std::allocator_traits::destroy(alloc, object); - std::allocator_traits::deallocate(alloc, object, 1); - break; - } - - case value_t::array: - { - AllocatorType alloc; - std::allocator_traits::destroy(alloc, array); - std::allocator_traits::deallocate(alloc, array, 1); - break; - } - case value_t::string: - { - AllocatorType alloc; - std::allocator_traits::destroy(alloc, string); - std::allocator_traits::deallocate(alloc, string, 1); + destroy_string(); break; - } case value_t::binary: - { - AllocatorType alloc; - std::allocator_traits::destroy(alloc, binary); - std::allocator_traits::deallocate(alloc, binary, 1); + destroy_binary(); + break; + + case value_t::object: + case value_t::array: + destroy_container(t); break; - } case value_t::null: case value_t::boolean: @@ -714,9 +824,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec case value_t::number_float: case value_t::discarded: default: - { break; - } } } }; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 3c7b9704d..86321a36e 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -27812,100 +27812,210 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// constructor for rvalue binary arrays (internal type) json_value(binary_t&& value) : binary(create(std::move(value))) {} - void destroy(value_t t) +private: + // raw, allocation-free transfer of m_data from src to dst: no + // set_parents()/assert_invariant() (the former is O(#children) per + // call under JSON_DIAGNOSTICS, which would make the walk below + // quadratic); dst takes ownership, src is left as value_t::null. + static void take(basic_json& dst, basic_json& src) noexcept + { + dst.m_data.m_type = src.m_data.m_type; + dst.m_data.m_value = src.m_data.m_value; + src.m_data.m_type = value_t::null; + } + + // true if v is not an array/object, or is an already-empty one + static bool has_no_children(const basic_json& v) noexcept + { + switch (v.m_data.m_type) + { + case value_t::array: + return v.m_data.m_value.array->empty(); + case value_t::object: + return v.m_data.m_value.object->empty(); + default: + return true; + } + } + + static basic_json& last_child(basic_json& v) + { + if (v.m_data.m_type == value_t::array) + { + return v.m_data.m_value.array->back(); + } + JSON_ASSERT(v.m_data.m_type == value_t::object); + return v.m_data.m_value.object->rbegin()->second; + } + + // removes the last child of a non-empty array/object v; this never + // allocates, and since it is only ever called when that child is a + // scalar or an already-empty array/object, destroying it never + // recurses more than one level deep (see destroy() below) + static void pop_last_child(basic_json& v) + { + if (v.m_data.m_type == value_t::array) + { + v.m_data.m_value.array->pop_back(); + } + else + { + JSON_ASSERT(v.m_data.m_type == value_t::object); + // erase() needs a forward iterator, so std::prev(end()) is + // used here rather than rbegin() (see last_child() above) + v.m_data.m_value.object->erase(std::prev(v.m_data.m_value.object->end())); + } + } + + // deallocates the (already empty) array/object held by v; this is + // the same allocator-based free the old recursive implementation + // used, just factored out so every level of the walk in destroy() + // can share it + static void free_container(basic_json& v) noexcept + { + if (v.m_data.m_type == value_t::array) + { + JSON_ASSERT(v.m_data.m_value.array->empty()); + AllocatorType alloc; + std::allocator_traits::destroy(alloc, v.m_data.m_value.array); + std::allocator_traits::deallocate(alloc, v.m_data.m_value.array, 1); + } + else + { + JSON_ASSERT(v.m_data.m_type == value_t::object); + JSON_ASSERT(v.m_data.m_value.object->empty()); + AllocatorType alloc; + std::allocator_traits::destroy(alloc, v.m_data.m_value.object); + std::allocator_traits::deallocate(alloc, v.m_data.m_value.object, 1); + } + v.m_data.m_type = value_t::null; // avoid a double free if v is later destructed + } + +public: + void destroy_string() noexcept + { + if (string == nullptr) + { + // not initialized (e.g., due to exception in the ctor) + return; + } + AllocatorType alloc; + std::allocator_traits::destroy(alloc, string); + std::allocator_traits::deallocate(alloc, string, 1); + } + + void destroy_binary() noexcept + { + if (binary == nullptr) + { + // not initialized (e.g., due to exception in the ctor) + return; + } + AllocatorType alloc; + std::allocator_traits::destroy(alloc, binary); + std::allocator_traits::deallocate(alloc, binary, 1); + } + + // t must be value_t::array or value_t::object + void destroy_container(value_t t) noexcept { if ( (t == value_t::object && object == nullptr) || - (t == value_t::array && array == nullptr) || - (t == value_t::string && string == nullptr) || - (t == value_t::binary && binary == nullptr) + (t == value_t::array && array == nullptr) ) { // not initialized (e.g., due to exception in the ctor) return; } - if (t == value_t::array || t == value_t::object) + + // Destroy the tree without recursing per nesting level and + // without any heap allocation: a heap-allocated flattening + // stack (the previous implementation) can itself throw + // bad_alloc, which would escape this noexcept destructor and + // terminate the program (#5135). + // + // Instead, walk down the "last child" chain, reversing links + // as we go: cur is the container currently being emptied, + // and prev is its parent (value_t::null when there is none). + // Each parent's last child slot doubles as storage for that + // parent's own parent link while we are below it, so no + // extra memory is needed. We only ever remove a child once + // it is a scalar or an empty array/object, which neither + // allocates nor recurses more than one level deep. + // + // This json_value is not itself a basic_json, so the + // top-level container is first moved into a local stand-in + // ("cur"); a default-constructed basic_json has a null + // pointer in its m_value (see data::m_value's initializer), + // so swapping it with *this leaves this union's own pointer + // null, and it is never looked at or freed a second time. + basic_json cur; + cur.m_data.m_type = t; + using std::swap; + swap(cur.m_data.m_value, *this); + + basic_json prev; // value_t::null: no parent + + while (true) { - // flatten the current json_value to a heap-allocated stack - std::vector stack; - - // move the top-level items to stack - if (t == value_t::array) + if (has_no_children(cur)) { - stack.reserve(array->size()); - std::move(array->begin(), array->end(), std::back_inserter(stack)); - } - else - { - stack.reserve(object->size()); - for (auto&& it : *object) + if (prev.m_data.m_type == value_t::null) { - stack.push_back(std::move(it.second)); - } - } - - while (!stack.empty()) - { - // move the last item to a local variable to be processed - basic_json current_item(std::move(stack.back())); - stack.pop_back(); - - // if current_item is array/object, move - // its children to the stack to be processed later - if (current_item.is_array()) - { - std::move(current_item.m_data.m_value.array->begin(), current_item.m_data.m_value.array->end(), std::back_inserter(stack)); - - current_item.m_data.m_value.array->clear(); - } - else if (current_item.is_object()) - { - for (auto&& it : *current_item.m_data.m_value.object) - { - stack.push_back(std::move(it.second)); - } - - current_item.m_data.m_value.object->clear(); + free_container(cur); + return; // back at the top with nothing left to do } - // it's now safe that current_item gets destructed - // since it doesn't have any children + // ascend: detach the grandparent link from prev's + // last slot, drop that (now null) slot, free cur + // (it is empty), then move up one level + basic_json gp; + take(gp, last_child(prev)); + pop_last_child(prev); + + free_container(cur); + + take(cur, prev); + take(prev, gp); + continue; } + + basic_json& cur_last_ref = last_child(cur); + + if (has_no_children(cur_last_ref)) + { + // scalar, or already-empty array/object + pop_last_child(cur); + continue; + } + + // descend into the non-empty last child, reversing the + // link: its slot takes over prev, and the child becomes + // the new cur + basic_json tmp; + take(tmp, cur_last_ref); + take(cur_last_ref, prev); + take(prev, cur); + take(cur, tmp); } + } + void destroy(value_t t) + { switch (t) { - case value_t::object: - { - AllocatorType alloc; - std::allocator_traits::destroy(alloc, object); - std::allocator_traits::deallocate(alloc, object, 1); - break; - } - - case value_t::array: - { - AllocatorType alloc; - std::allocator_traits::destroy(alloc, array); - std::allocator_traits::deallocate(alloc, array, 1); - break; - } - case value_t::string: - { - AllocatorType alloc; - std::allocator_traits::destroy(alloc, string); - std::allocator_traits::deallocate(alloc, string, 1); + destroy_string(); break; - } case value_t::binary: - { - AllocatorType alloc; - std::allocator_traits::destroy(alloc, binary); - std::allocator_traits::deallocate(alloc, binary, 1); + destroy_binary(); + break; + + case value_t::object: + case value_t::array: + destroy_container(t); break; - } case value_t::null: case value_t::boolean: @@ -27914,9 +28024,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec case value_t::number_float: case value_t::discarded: default: - { break; - } } } }; diff --git a/tests/src/unit-allocator.cpp b/tests/src/unit-allocator.cpp index a358e75f9..c716b3a5b 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -607,3 +607,88 @@ TEST_CASE("bad my_allocator::construct") j["test"].push_back("should not leak"); } } + +namespace +{ +std::size_t counting_allocator_allocations = 0; +std::size_t counting_allocator_deallocations = 0; + +template +struct counting_allocator : std::allocator +{ + using std::allocator::allocator; + + T* allocate(std::size_t n) + { + ++counting_allocator_allocations; + return std::allocator::allocate(n); + } + + void deallocate(T* p, std::size_t n) + { + ++counting_allocator_deallocations; + std::allocator::deallocate(p, n); + } + + template + struct rebind + { + using other = counting_allocator; + }; +}; +} // namespace + +TEST_CASE("destructor performs no allocation, only deallocation") +{ + // see https://github.com/nlohmann/json/issues/4842 and + // https://github.com/nlohmann/json/issues/5135: destroying nested + // arrays/objects used to allocate a temporary stack (first with + // std::allocator, later - after #4842 - with the provided allocator). + // Since that stack could itself throw bad_alloc from inside the + // noexcept destructor (#5135), destroy() no longer allocates anything: + // it only ever frees what is already there. + using counting_json = nlohmann::basic_json; + + SECTION("array") + { + auto* j = new counting_json({1, {2, {3, 4}}, 5}); // NOLINT(cppcoreguidelines-owning-memory) + const auto allocations_before = counting_allocator_allocations; + const auto deallocations_before = counting_allocator_deallocations; + delete j; // NOLINT(cppcoreguidelines-owning-memory) + CHECK(counting_allocator_allocations == allocations_before); + CHECK(counting_allocator_deallocations > deallocations_before); + } + + SECTION("object") + { + auto* j = new counting_json({{"a", {{"b", {1, 2}}}}, {"c", 3}}); // NOLINT(cppcoreguidelines-owning-memory) + const auto allocations_before = counting_allocator_allocations; + const auto deallocations_before = counting_allocator_deallocations; + delete j; // NOLINT(cppcoreguidelines-owning-memory) + CHECK(counting_allocator_allocations == allocations_before); + CHECK(counting_allocator_deallocations > deallocations_before); + } + + SECTION("mixed tree of empty/non-empty arrays and objects") + { + auto* j = new counting_json( // NOLINT(cppcoreguidelines-owning-memory) + { + {"empty_obj", counting_json::object()}, + {"empty_arr", counting_json::array()}, + {"nested", {{"a", counting_json::array({1, 2, counting_json::object()})}, {"b", 3}}}, + {"tail", counting_json::array({counting_json::array({1}), 2, counting_json::array({3})})} + }); + const auto allocations_before = counting_allocator_allocations; + const auto deallocations_before = counting_allocator_deallocations; + delete j; // NOLINT(cppcoreguidelines-owning-memory) + CHECK(counting_allocator_allocations == allocations_before); + CHECK(counting_allocator_deallocations > deallocations_before); + } +} diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 1a8c73d4e..eb140f157 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -40,7 +40,9 @@ using ordered_json = nlohmann::ordered_json; #endif #include +#include #include +#include #include #include #include @@ -107,6 +109,84 @@ DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") using float_json = nlohmann::basic_json; +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) +namespace +{ +// An allocator whose allocate() can be told to fail on demand, so tests can +// check that ~basic_json() tolerates - in fact, after #5135, never even +// triggers - an allocation failure. This replaces an earlier version of +// this test that overrode the process-wide ::operator new/::operator +// delete, which affected every allocation in the whole unit-regression2 +// binary rather than just the values under test. +std::size_t failing_allocator_allocations = 0; +std::size_t failing_allocator_deallocations = 0; +bool fail_next_allocation = false; + +template +struct failing_allocator : std::allocator +{ + using std::allocator::allocator; + + failing_allocator() noexcept = default; + template + failing_allocator(const failing_allocator& /*unused*/) noexcept {} // NOLINT(google-explicit-constructor) + + T* allocate(std::size_t n) + { + if (fail_next_allocation) + { + fail_next_allocation = false; + throw std::bad_alloc(); + } + ++failing_allocator_allocations; + return std::allocator::allocate(n); + } + + void deallocate(T* p, std::size_t n) + { + ++failing_allocator_deallocations; + std::allocator::deallocate(p, n); + } + + template + struct rebind + { + using other = failing_allocator; + }; +}; + +using failing_json = nlohmann::basic_json; +using failing_ordered_json = nlohmann::basic_json; + +// builds `depth` levels of nesting around a scalar, iteratively (never +// recursing: each wrap only moves the previous, already-built value, which +// is O(1)), each level an array or an object depending on `nest_objects` +template +BasicJsonType make_deep_nest(std::size_t depth, bool nest_objects) +{ + BasicJsonType v = 0; + for (std::size_t i = 0; i < depth; ++i) + { + if (nest_objects) + { + BasicJsonType wrapper = BasicJsonType::object(); + wrapper["x"] = std::move(v); + v = std::move(wrapper); + } + else + { + BasicJsonType wrapper = BasicJsonType::array(); + wrapper.push_back(std::move(v)); + v = std::move(wrapper); + } + } + return v; +} +} // namespace +#endif + ///////////////////////////////////////////////////////////////////// // for #1647 ///////////////////////////////////////////////////////////////////// @@ -940,4 +1020,211 @@ TEST_CASE("regression test - excessive binary container size honors allow_except CHECK(json::from_cbor(std::vector {0x9b, 0, 0, 0, 0, 0, 0, 0, 0x02}, true, false).is_discarded()); } +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) +TEST_CASE("regression test #5135 - destructor never allocates, even under memory pressure") +{ + // Before the fix, ~basic_json() flattened a nested array/object into a + // heap-allocated std::vector to avoid recursing; that allocation could + // itself throw bad_alloc, which escapes a noexcept destructor and + // terminates the program. destroy() no longer allocates anything, so + // none of the sections below ever observe fail_next_allocation being + // consumed: CHECK(fail_next_allocation) confirms it was never touched. + + SECTION("the original report: a small, mixed array/object nest") + { + failing_allocator_allocations = 0; + failing_allocator_deallocations = 0; + { + failing_json j = failing_json::array( + { + failing_json::array({1, 2}), + failing_json::object({{"key", failing_json::array({3})}}) + }); + fail_next_allocation = true; + } // j is destroyed here, with every further allocation set to fail + + CHECK(fail_next_allocation); + fail_next_allocation = false; + CHECK(failing_allocator_deallocations > 0); + } + + SECTION("100000-deep nested array") + { + std::size_t allocations_before = 0; + { + failing_json j = make_deep_nest(100000, false); + allocations_before = failing_allocator_allocations; + fail_next_allocation = true; + } + + CHECK(fail_next_allocation); + fail_next_allocation = false; + CHECK(failing_allocator_allocations == allocations_before); + } + + SECTION("100000-deep nested object") + { + std::size_t allocations_before = 0; + { + failing_json j = make_deep_nest(100000, true); + allocations_before = failing_allocator_allocations; + fail_next_allocation = true; + } + + CHECK(fail_next_allocation); + fail_next_allocation = false; + CHECK(failing_allocator_allocations == allocations_before); + } + + SECTION("100000-deep nested ordered_json") + { + std::size_t allocations_before = 0; + { + failing_ordered_json j = make_deep_nest(100000, true); + allocations_before = failing_allocator_allocations; + fail_next_allocation = true; + } + + CHECK(fail_next_allocation); + fail_next_allocation = false; + CHECK(failing_allocator_allocations == allocations_before); + } + + SECTION("wide and deep: 1000 arrays of 1000 elements, each a small nested object") + { + std::size_t allocations_before = 0; + { + failing_json wide = failing_json::array(); + for (std::size_t i = 0; i < 1000; ++i) + { + failing_json inner = failing_json::array(); + for (std::size_t k = 0; k < 1000; ++k) + { + inner.push_back(failing_json::object({{"a", 1}, {"b", failing_json::array({1, 2, 3})}})); + } + wide.push_back(std::move(inner)); + } + + allocations_before = failing_allocator_allocations; + fail_next_allocation = true; + } + + CHECK(fail_next_allocation); + fail_next_allocation = false; + CHECK(failing_allocator_allocations == allocations_before); + } +} +#endif + +namespace +{ +// a single-element chain of `depth` arrays, built iteratively (never +// recursing: each wrap only moves the previous, already-built value) +template +BasicJsonType make_single_chain(std::size_t depth) +{ + BasicJsonType v = 1; + for (std::size_t i = 0; i < depth; ++i) + { + BasicJsonType wrapper = BasicJsonType::array(); + wrapper.push_back(std::move(v)); + v = std::move(wrapper); + } + return v; +} + +// copies value first, to make sure nothing was corrupted by building it, +// then lets both the copy and the original destruct via normal scope exit +template +void check_destroy_edge_case(const BasicJsonType& value) +{ + const BasicJsonType copy = value; + CHECK(copy == value); +} +} // namespace + +TEST_CASE_TEMPLATE("regression test #5135 - destroy() edge cases", BasicJsonType, json, ordered_json) +{ + using binary_t = typename BasicJsonType::binary_t; + + SECTION("mix of empty objects, empty arrays, non-empty containers, and scalars") + { + BasicJsonType root = BasicJsonType::array(); + root.push_back(BasicJsonType::object()); + root.push_back(BasicJsonType::array()); + root.push_back(BasicJsonType::object({{"k", 1}})); + root.push_back(BasicJsonType::array({1, 2, 3})); + root.push_back(nullptr); + root.push_back(true); + root.push_back(42); + root.push_back(3.14); + root.push_back("a string"); + root.push_back(BasicJsonType(binary_t({1, 2, 3}))); + check_destroy_edge_case(root); + } + + SECTION("container child in first position only") + { + BasicJsonType root = BasicJsonType::array({BasicJsonType::array({1, 2}), 3, 4, 5}); + check_destroy_edge_case(root); + } + + SECTION("container child in last position only") + { + BasicJsonType root = BasicJsonType::array({1, 2, 3, BasicJsonType::array({4, 5})}); + check_destroy_edge_case(root); + } + + SECTION("container children in first and last position") + { + BasicJsonType root = BasicJsonType::array({BasicJsonType::array({1}), 2, 3, BasicJsonType::array({4})}); + check_destroy_edge_case(root); + } + + SECTION("single-element chain, 1000 levels deep") + { + BasicJsonType root = make_single_chain(1000); + check_destroy_edge_case(root); + } + + SECTION("top-level empty array") + { + BasicJsonType root = BasicJsonType::array(); + check_destroy_edge_case(root); + } + + SECTION("top-level empty object") + { + BasicJsonType root = BasicJsonType::object(); + check_destroy_edge_case(root); + } + + SECTION("object whose last child is a non-empty array whose last child is an empty object") + { + BasicJsonType inner_array = BasicJsonType::array({1, 2, BasicJsonType::object()}); + BasicJsonType root = BasicJsonType::object({{"a", 1}, {"b", inner_array}}); + check_destroy_edge_case(root); + } + + SECTION("destruction via erase() on a deeply nested child") + { + BasicJsonType root = BasicJsonType::array(); + root.push_back(make_single_chain(500)); + root.push_back(BasicJsonType::object({{"k", BasicJsonType::array({1, 2, 3})}})); + // erase() must destroy the removed subtree without recursing or + // allocating beyond what erase() itself needs + root.erase(0); + CAPTURE(root.size()) + CHECK(root.size() == 1); + } + + SECTION("destruction via assignment on a deep tree") + { + BasicJsonType root = make_single_chain(2000); + // assigning a new value destroys the old one in place + root = nullptr; + CHECK(root.is_null()); + } +} + DOCTEST_CLANG_SUPPRESS_WARNING_POP