From 6d0867a85f0148ae184f457ec6755f9d4e184132 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 2 Oct 2026 17:25:17 +0200 Subject: [PATCH 01/12] Make comparisons with scalars noexcept only when the conversion is (#5751) The comparison operators taking a scalar (==, !=, <, <=, >, >=, and C++20's <=>) convert the scalar to a basic_json and compare, but were unconditionally noexcept. When that conversion throws, the program called std::terminate instead of propagating the exception, e.g. when comparing a json with a string literal under memory pressure (std::bad_alloc) or with an enum value not mapped by NLOHMANN_JSON_SERIALIZE_ENUM_STRICT (out_of_range.410). clang-tidy 22.1 reports the latter as bugprone-exception-escape. Declare the 16 scalar overloads noexcept(std::is_nothrow_constructible::value): they stay noexcept for numbers, Booleans, nullptr, and plain enums, and are noexcept(false) for strings and enums whose to_json may throw. The comparisons of two basic_json values are unchanged. Restore the strict-enum comparisons removed from unit-conversions.cpp in the previous PR, check that comparing an unmapped strict enum now throws, and pin the new exception specifications in unit-noexcept.cpp. Document the exception safety of overload (2) on all seven operator pages. Ran make amalgamate. Signed-off-by: Niels Lohmann --- .../mkdocs/docs/api/basic_json/operator_eq.md | 14 +++++--- .../mkdocs/docs/api/basic_json/operator_ge.md | 12 +++++-- .../mkdocs/docs/api/basic_json/operator_gt.md | 12 +++++-- .../mkdocs/docs/api/basic_json/operator_le.md | 12 +++++-- .../mkdocs/docs/api/basic_json/operator_lt.md | 12 +++++-- .../mkdocs/docs/api/basic_json/operator_ne.md | 12 +++++-- .../docs/api/basic_json/operator_spaceship.md | 10 ++++-- include/nlohmann/json.hpp | 32 +++++++++---------- single_include/nlohmann/json.hpp | 32 +++++++++---------- tests/src/unit-conversions.cpp | 21 ++++++++++++ tests/src/unit-noexcept.cpp | 9 ++++++ 11 files changed, 125 insertions(+), 53 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/operator_eq.md b/docs/mkdocs/docs/api/basic_json/operator_eq.md index 26eda720f..58a7e74da 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_eq.md +++ b/docs/mkdocs/docs/api/basic_json/operator_eq.md @@ -5,17 +5,17 @@ bool operator==(const_reference lhs, const_reference rhs) noexcept; // (1) template -bool operator==(const_reference lhs, const ScalarType rhs) noexcept; // (2) +bool operator==(const_reference lhs, const ScalarType rhs) noexcept(/* see below */); // (2) template -bool operator==(ScalarType lhs, const const_reference rhs) noexcept; // (2) +bool operator==(ScalarType lhs, const const_reference rhs) noexcept(/* see below */); // (2) // since C++20 class basic_json { bool operator==(const_reference rhs) const noexcept; // (1) template - bool operator==(ScalarType rhs) const noexcept; // (2) + bool operator==(ScalarType rhs) const noexcept(/* see below */); // (2) }; ``` @@ -46,7 +46,12 @@ whether the values `lhs`/`*this` and `rhs` are equal ## Exception safety -No-throw guarantee: this function never throws exceptions. +1. No-throw guarantee: this function never throws exceptions. +2. No-throw guarantee if converting the scalar to a JSON value cannot throw, as for numbers, Booleans, and + `#!cpp nullptr`; the function is `#!cpp noexcept` exactly in that case. Otherwise, it throws what the conversion + throws, for example `std::bad_alloc` when converting a string, or + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) for an enum value not mapped by + [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](../macros/nlohmann_json_serialize_enum_strict.md). ## Complexity @@ -171,3 +176,4 @@ Linear. 1. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. 2. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. + Made conditionally `#!cpp noexcept` in version 3.13.0; before, a throwing conversion called `std::terminate`. diff --git a/docs/mkdocs/docs/api/basic_json/operator_ge.md b/docs/mkdocs/docs/api/basic_json/operator_ge.md index f7899beab..99949f226 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_ge.md +++ b/docs/mkdocs/docs/api/basic_json/operator_ge.md @@ -5,10 +5,10 @@ bool operator>=(const_reference lhs, const_reference rhs) noexcept; // (1) template -bool operator>=(const_reference lhs, const ScalarType rhs) noexcept; // (2) +bool operator>=(const_reference lhs, const ScalarType rhs) noexcept(/* see below */); // (2) template -bool operator>=(ScalarType lhs, const const_reference rhs) noexcept; // (2) +bool operator>=(ScalarType lhs, const const_reference rhs) noexcept(/* see below */); // (2) ``` 1. Compares whether one JSON value `lhs` is greater than or equal to another JSON value `rhs` according to the following @@ -39,7 +39,12 @@ whether `lhs` is greater than or equal to `rhs` ## Exception safety -No-throw guarantee: this function never throws exceptions. +1. No-throw guarantee: this function never throws exceptions. +2. No-throw guarantee if converting the scalar to a JSON value cannot throw, as for numbers, Booleans, and + `#!cpp nullptr`; the function is `#!cpp noexcept` exactly in that case. Otherwise, it throws what the conversion + throws, for example `std::bad_alloc` when converting a string, or + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) for an enum value not mapped by + [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](../macros/nlohmann_json_serialize_enum_strict.md). ## Complexity @@ -94,3 +99,4 @@ Linear. 1. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0. 2. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0. + Made conditionally `#!cpp noexcept` in version 3.13.0; before, a throwing conversion called `std::terminate`. diff --git a/docs/mkdocs/docs/api/basic_json/operator_gt.md b/docs/mkdocs/docs/api/basic_json/operator_gt.md index 486da5fd0..3fcb339da 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_gt.md +++ b/docs/mkdocs/docs/api/basic_json/operator_gt.md @@ -5,10 +5,10 @@ bool operator>(const_reference lhs, const_reference rhs) noexcept; // (1) template -bool operator>(const_reference lhs, const ScalarType rhs) noexcept; // (2) +bool operator>(const_reference lhs, const ScalarType rhs) noexcept(/* see below */); // (2) template -bool operator>(ScalarType lhs, const const_reference rhs) noexcept; // (2) +bool operator>(ScalarType lhs, const const_reference rhs) noexcept(/* see below */); // (2) ``` 1. Compares whether one JSON value `lhs` is greater than another JSON value `rhs` according to the @@ -39,7 +39,12 @@ whether `lhs` is greater than `rhs` ## Exception safety -No-throw guarantee: this function never throws exceptions. +1. No-throw guarantee: this function never throws exceptions. +2. No-throw guarantee if converting the scalar to a JSON value cannot throw, as for numbers, Booleans, and + `#!cpp nullptr`; the function is `#!cpp noexcept` exactly in that case. Otherwise, it throws what the conversion + throws, for example `std::bad_alloc` when converting a string, or + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) for an enum value not mapped by + [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](../macros/nlohmann_json_serialize_enum_strict.md). ## Complexity @@ -84,3 +89,4 @@ Linear. 1. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0. 2. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0. + Made conditionally `#!cpp noexcept` in version 3.13.0; before, a throwing conversion called `std::terminate`. diff --git a/docs/mkdocs/docs/api/basic_json/operator_le.md b/docs/mkdocs/docs/api/basic_json/operator_le.md index 4334fe35e..db193d0db 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_le.md +++ b/docs/mkdocs/docs/api/basic_json/operator_le.md @@ -5,10 +5,10 @@ bool operator<=(const_reference lhs, const_reference rhs) noexcept; // (1) template -bool operator<=(const_reference lhs, const ScalarType rhs) noexcept; // (2) +bool operator<=(const_reference lhs, const ScalarType rhs) noexcept(/* see below */); // (2) template -bool operator<=(ScalarType lhs, const const_reference rhs) noexcept; // (2) +bool operator<=(ScalarType lhs, const const_reference rhs) noexcept(/* see below */); // (2) ``` 1. Compares whether one JSON value `lhs` is less than or equal to another JSON value `rhs` @@ -40,7 +40,12 @@ whether `lhs` is less than or equal to `rhs` ## Exception safety -No-throw guarantee: this function never throws exceptions. +1. No-throw guarantee: this function never throws exceptions. +2. No-throw guarantee if converting the scalar to a JSON value cannot throw, as for numbers, Booleans, and + `#!cpp nullptr`; the function is `#!cpp noexcept` exactly in that case. Otherwise, it throws what the conversion + throws, for example `std::bad_alloc` when converting a string, or + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) for an enum value not mapped by + [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](../macros/nlohmann_json_serialize_enum_strict.md). ## Complexity @@ -95,3 +100,4 @@ Linear. 1. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0. 2. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0. + Made conditionally `#!cpp noexcept` in version 3.13.0; before, a throwing conversion called `std::terminate`. diff --git a/docs/mkdocs/docs/api/basic_json/operator_lt.md b/docs/mkdocs/docs/api/basic_json/operator_lt.md index 118d817c8..1b8e65225 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_lt.md +++ b/docs/mkdocs/docs/api/basic_json/operator_lt.md @@ -5,10 +5,10 @@ bool operator<(const_reference lhs, const_reference rhs) noexcept; // (1) template -bool operator<(const_reference lhs, const ScalarType rhs) noexcept; // (2) +bool operator<(const_reference lhs, const ScalarType rhs) noexcept(/* see below */); // (2) template -bool operator<(ScalarType lhs, const const_reference rhs) noexcept; // (2) +bool operator<(ScalarType lhs, const const_reference rhs) noexcept(/* see below */); // (2) ``` 1. Compares whether one JSON value `lhs` is less than another JSON value `rhs` according to the @@ -49,7 +49,12 @@ whether `lhs` is less than `rhs` ## Exception safety -No-throw guarantee: this function never throws exceptions. +1. No-throw guarantee: this function never throws exceptions. +2. No-throw guarantee if converting the scalar to a JSON value cannot throw, as for numbers, Booleans, and + `#!cpp nullptr`; the function is `#!cpp noexcept` exactly in that case. Otherwise, it throws what the conversion + throws, for example `std::bad_alloc` when converting a string, or + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) for an enum value not mapped by + [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](../macros/nlohmann_json_serialize_enum_strict.md). ## Complexity @@ -94,3 +99,4 @@ Linear. 1. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0. 2. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0. + Made conditionally `#!cpp noexcept` in version 3.13.0; before, a throwing conversion called `std::terminate`. diff --git a/docs/mkdocs/docs/api/basic_json/operator_ne.md b/docs/mkdocs/docs/api/basic_json/operator_ne.md index a8c6fecc2..ceeb31ab3 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_ne.md +++ b/docs/mkdocs/docs/api/basic_json/operator_ne.md @@ -5,10 +5,10 @@ bool operator!=(const_reference lhs, const_reference rhs) noexcept; // (1) template -bool operator!=(const_reference lhs, const ScalarType rhs) noexcept; // (2) +bool operator!=(const_reference lhs, const ScalarType rhs) noexcept(/* see below */); // (2) template -bool operator!=(ScalarType lhs, const const_reference rhs) noexcept; // (2) +bool operator!=(ScalarType lhs, const const_reference rhs) noexcept(/* see below */); // (2) ``` 1. Compares two JSON values for inequality. Returns `#!cpp !(lhs == rhs)`. @@ -36,7 +36,12 @@ whether the values `lhs`/`*this` and `rhs` are not equal ## Exception safety -No-throw guarantee: this function never throws exceptions. +1. No-throw guarantee: this function never throws exceptions. +2. No-throw guarantee if converting the scalar to a JSON value cannot throw, as for numbers, Booleans, and + `#!cpp nullptr`; the function is `#!cpp noexcept` exactly in that case. Otherwise, it throws what the conversion + throws, for example `std::bad_alloc` when converting a string, or + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) for an enum value not mapped by + [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](../macros/nlohmann_json_serialize_enum_strict.md). ## Complexity @@ -98,3 +103,4 @@ Linear. member function in version 3.13.0; since C++20, the compiler rewrites `a != b` using `operator==`. 2. Added in version 1.0.0. Changed in version 3.13.0 to remove special-casing for `NaN` and `discarded` values; `operator!=` now consistently means `!(a == b)`. Since C++20, the compiler rewrites `a != b` using `operator==`. + Made conditionally `#!cpp noexcept` in version 3.13.0; before, a throwing conversion called `std::terminate`. diff --git a/docs/mkdocs/docs/api/basic_json/operator_spaceship.md b/docs/mkdocs/docs/api/basic_json/operator_spaceship.md index 9e91d0d2d..47ca22484 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_spaceship.md +++ b/docs/mkdocs/docs/api/basic_json/operator_spaceship.md @@ -6,7 +6,7 @@ class basic_json { std::partial_ordering operator<=>(const_reference rhs) const noexcept; // (1) template - std::partial_ordering operator<=>(const ScalarType rhs) const noexcept; // (2) + std::partial_ordering operator<=>(const ScalarType rhs) const noexcept(/* see below */); // (2) }; ``` @@ -39,7 +39,12 @@ the `std::partial_ordering` of the 3-way comparison of `*this` and `rhs` ## Exception safety -No-throw guarantee: this function never throws exceptions. +1. No-throw guarantee: this function never throws exceptions. +2. No-throw guarantee if converting the scalar to a JSON value cannot throw, as for numbers, Booleans, and + `#!cpp nullptr`; the function is `#!cpp noexcept` exactly in that case. Otherwise, it throws what the conversion + throws, for example `std::bad_alloc` when converting a string, or + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) for an enum value not mapped by + [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](../macros/nlohmann_json_serialize_enum_strict.md). ## Complexity @@ -98,3 +103,4 @@ Linear. 1. Added in version 3.11.0. 2. Added in version 3.11.0. + Made conditionally `#!cpp noexcept` in version 3.13.0; before, a throwing conversion called `std::terminate`. diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index d0f779109..bb78b211d 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -4865,7 +4865,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_eq/ template requires std::is_scalar_v - bool operator==(ScalarType rhs) const noexcept + bool operator==(ScalarType rhs) const noexcept(std::is_nothrow_constructible::value) { return *this == basic_json(rhs); } @@ -4888,7 +4888,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/ template requires std::is_scalar_v - std::partial_ordering operator<=>(ScalarType rhs) const noexcept // *NOPAD* + std::partial_ordering operator<=>(ScalarType rhs) const noexcept(std::is_nothrow_constructible::value) // *NOPAD* { return *this <=> basic_json(rhs); // *NOPAD* } @@ -4913,7 +4913,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 - bool operator<=(ScalarType rhs) const noexcept + bool operator<=(ScalarType rhs) const noexcept(std::is_nothrow_constructible::value) { return *this <= basic_json(rhs); } @@ -4934,7 +4934,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 - bool operator>=(ScalarType rhs) const noexcept + bool operator>=(ScalarType rhs) const noexcept(std::is_nothrow_constructible::value) { return *this >= basic_json(rhs); } @@ -4959,7 +4959,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==(const_reference lhs, ScalarType rhs) noexcept + friend bool operator==(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs == basic_json(rhs); } @@ -4968,7 +4968,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 + friend bool operator==(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) == rhs; } @@ -4984,7 +4984,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 + friend bool operator!=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs != basic_json(rhs); } @@ -4993,7 +4993,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 + friend bool operator!=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) != rhs; } @@ -5013,7 +5013,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 + friend bool operator<(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs < basic_json(rhs); } @@ -5022,7 +5022,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 + friend bool operator<(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) < rhs; } @@ -5042,7 +5042,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 + friend bool operator<=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs <= basic_json(rhs); } @@ -5051,7 +5051,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 + friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) <= rhs; } @@ -5072,7 +5072,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 + friend bool operator>(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs > basic_json(rhs); } @@ -5081,7 +5081,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 + friend bool operator>(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) > rhs; } @@ -5101,7 +5101,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 + friend bool operator>=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs >= basic_json(rhs); } @@ -5110,7 +5110,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 + friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) >= rhs; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 5a6f5afd7..c4f3313ae 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -31564,7 +31564,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_eq/ template requires std::is_scalar_v - bool operator==(ScalarType rhs) const noexcept + bool operator==(ScalarType rhs) const noexcept(std::is_nothrow_constructible::value) { return *this == basic_json(rhs); } @@ -31587,7 +31587,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/ template requires std::is_scalar_v - std::partial_ordering operator<=>(ScalarType rhs) const noexcept // *NOPAD* + std::partial_ordering operator<=>(ScalarType rhs) const noexcept(std::is_nothrow_constructible::value) // *NOPAD* { return *this <=> basic_json(rhs); // *NOPAD* } @@ -31612,7 +31612,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 - bool operator<=(ScalarType rhs) const noexcept + bool operator<=(ScalarType rhs) const noexcept(std::is_nothrow_constructible::value) { return *this <= basic_json(rhs); } @@ -31633,7 +31633,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 - bool operator>=(ScalarType rhs) const noexcept + bool operator>=(ScalarType rhs) const noexcept(std::is_nothrow_constructible::value) { return *this >= basic_json(rhs); } @@ -31658,7 +31658,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==(const_reference lhs, ScalarType rhs) noexcept + friend bool operator==(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs == basic_json(rhs); } @@ -31667,7 +31667,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 + friend bool operator==(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) == rhs; } @@ -31683,7 +31683,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 + friend bool operator!=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs != basic_json(rhs); } @@ -31692,7 +31692,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 + friend bool operator!=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) != rhs; } @@ -31712,7 +31712,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 + friend bool operator<(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs < basic_json(rhs); } @@ -31721,7 +31721,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 + friend bool operator<(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) < rhs; } @@ -31741,7 +31741,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 + friend bool operator<=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs <= basic_json(rhs); } @@ -31750,7 +31750,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 + friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) <= rhs; } @@ -31771,7 +31771,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 + friend bool operator>(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs > basic_json(rhs); } @@ -31780,7 +31780,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 + friend bool operator>(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) > rhs; } @@ -31800,7 +31800,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 + friend bool operator>=(const_reference lhs, ScalarType rhs) noexcept(std::is_nothrow_constructible::value) { return lhs >= basic_json(rhs); } @@ -31809,7 +31809,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 + friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept(std::is_nothrow_constructible::value) { return basic_json(lhs) >= rhs; } diff --git a/tests/src/unit-conversions.cpp b/tests/src/unit-conversions.cpp index ff6e5e5c7..df8685529 100644 --- a/tests/src/unit-conversions.cpp +++ b/tests/src/unit-conversions.cpp @@ -1764,6 +1764,12 @@ TEST_CASE("Strict JSON to enum mapping") CHECK(json("herz").get() == strict_cards::herz); CHECK(json("karo").get() == strict_cards::karo); + // comparison of enum and json + CHECK(strict_cards::kreuz == json("kreuz")); + CHECK(strict_cards::pik == json("pik")); + CHECK(strict_cards::herz == json("herz")); + CHECK(strict_cards::karo == json("karo")); + // invalid json -> exception thrown json _; CHECK_THROWS_WITH_AS(_ = json("what?").get(), "[json.exception.out_of_range.410] enum value out of range for strict_cards: \"what?\"", json::out_of_range&); @@ -1771,6 +1777,12 @@ TEST_CASE("Strict JSON to enum mapping") // conversion of unmapped enum -> exception thrown CHECK_THROWS_WITH_AS(json(strict_cards::andere), "[json.exception.out_of_range.410] enum value out of range for strict_cards", json::out_of_range&); + // comparing an unmapped enum with json throws the same exception + // (the scalar comparison operators used to be noexcept, so this + // called std::terminate) + CHECK_THROWS_WITH_AS(static_cast(strict_cards::andere == json("andere")), "[json.exception.out_of_range.410] enum value out of range for strict_cards", json::out_of_range&); + CHECK_THROWS_WITH_AS(static_cast(json("andere") != strict_cards::andere), "[json.exception.out_of_range.410] enum value out of range for strict_cards", json::out_of_range&); + // invalid UTF-8 -> out_of_range.410, not the type_error.316 thrown while building the // message (regression test for #5667); such strings can reach get() unvalidated, // e.g. from from_cbor()/from_msgpack() (#5529) @@ -1792,12 +1804,21 @@ TEST_CASE("Strict JSON to enum mapping") CHECK(json("completed").get() == STRICT_TS_COMPLETED); CHECK(json().get() == STRICT_TS_INVALID); + // comparison of enum and json + CHECK(STRICT_TS_STOPPED == json("stopped")); + CHECK(STRICT_TS_RUNNING == json("running")); + CHECK(STRICT_TS_COMPLETED == json("completed")); + CHECK(STRICT_TS_INVALID == json()); + // invalid json -> exception thrown json _; CHECK_THROWS_WITH_AS(_ = json("what?").get(), "[json.exception.out_of_range.410] enum value out of range for StrictTaskState: \"what?\"", json::out_of_range&); // conversion of unmapped enum -> exception thrown CHECK_THROWS_WITH_AS(json(STRICT_TS_OTHER), "[json.exception.out_of_range.410] enum value out of range for StrictTaskState", json::out_of_range&); + + // comparing an unmapped enum with json throws the same exception + CHECK_THROWS_WITH_AS(static_cast(STRICT_TS_OTHER < json("x")), "[json.exception.out_of_range.410] enum value out of range for StrictTaskState", json::out_of_range&); } } diff --git a/tests/src/unit-noexcept.cpp b/tests/src/unit-noexcept.cpp index 637915f24..85361df24 100644 --- a/tests/src/unit-noexcept.cpp +++ b/tests/src/unit-noexcept.cpp @@ -54,6 +54,15 @@ static_assert(noexcept(json(pod {})), ""); static_assert(noexcept(std::declval().get()), ""); static_assert(!noexcept(std::declval().get()), ""); static_assert(noexcept(json(pod{})), ""); + +// comparing with a scalar is noexcept exactly when converting the scalar is +static_assert(noexcept(std::declval() == 1), ""); +static_assert(noexcept(1 != std::declval()), ""); +static_assert(noexcept(std::declval() < 2.5), ""); +static_assert(noexcept(nullptr == std::declval()), ""); +static_assert(!noexcept(std::declval() == "foo"), ""); +static_assert(!noexcept("foo" >= std::declval()), ""); +static_assert(noexcept(std::declval() == std::declval()), ""); } // namespace TEST_CASE("noexcept") From 78ddd95794881a2830fa47972f8b6bcfba842c13 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:13 +0200 Subject: [PATCH 02/12] Document GCC < 11 incomplete-type error with optional members (#3669) (#5596) * Document GCC < 11 incomplete-type error with optional members (#3669) With GCC 10 and older in C++11/14 mode, a free to_json() for a type holding an optional (Dummy constructible from json) fails with "invalid use of incomplete type detector<...to_json_function...>". ADL for Dummy finds the unrelated to_json and closes an instantiation cycle through optional's converting constructor. The same error reproduces without the library, so it can't be fixed here. Add a FAQ entry explaining the cause and the hidden-friend workaround, recommend hidden friends in the arbitrary types docs, and add a regression test that keeps the workaround compiling on GCC 7-10. Signed-off-by: Niels Lohmann * Use the #3669 fixture's optional member in to_json Issue3669Holder::d is never read, so clang's -Weverything -Werror build (ci_test_clang) fails with -Wunused-private-field. Reference the member in the hidden-friend to_json; this does not affect the instantiation cycle the fixture reproduces. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/features/arbitrary_types.md | 1 + docs/mkdocs/docs/home/faq.md | 45 ++++++++++++++++ tests/src/unit-regression2.cpp | 54 ++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/docs/mkdocs/docs/features/arbitrary_types.md b/docs/mkdocs/docs/features/arbitrary_types.md index f7f3326ce..53f9cbd07 100644 --- a/docs/mkdocs/docs/features/arbitrary_types.md +++ b/docs/mkdocs/docs/features/arbitrary_types.md @@ -79,6 +79,7 @@ Some important things: * When using `get()`, `your_type` **MUST** be [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible). (There is a way to bypass this requirement described later.) * In function `from_json`, use function [`at()`](../api/basic_json/at.md) to access the object values rather than `operator[]`. In case a key does not exist, `at` throws an exception that you can handle, whereas `operator[]` exhibits undefined behavior. * You do not need to add serializers or deserializers for STL types like `std::vector`: the library already implements these. +* If you control the type, consider defining `to_json`/`from_json` as `friend` functions inside the class ("hidden friends"). Argument-dependent lookup then only finds them for your type, which also avoids a [GCC < 11 compilation error](../home/faq.md#incomplete-detector-type-with-gcc-11). ??? example "Example: serialize a `person` to JSON with `to_json`" diff --git a/docs/mkdocs/docs/home/faq.md b/docs/mkdocs/docs/home/faq.md index ca113004c..852fd8ffd 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -305,6 +305,51 @@ Only very old NDKs (before r18), which defaulted to GCC and `gnustl`, lacked C++ `std::to_string`. If you run into this, update to a current NDK. +### Incomplete `detector` type with GCC < 11 + +!!! question + + Why does GCC 10 or older fail with `invalid use of incomplete type 'struct nlohmann::detail::detector<..., to_json_function, ...>'` for a type that holds an `optional` member? + +This happens with GCC 10 and older in C++11/C++14 mode when all of these hold: + +- a class `Holder` has an `optional` member (e.g., `boost::optional`), +- `Dummy` has a constructor taking a `json` value, and +- `to_json` for `Holder` is a free function in the namespace of `Dummy`. + +```cpp +class Dummy { + public: + explicit Dummy(const nlohmann::json& j); +}; + +class Holder { + boost::optional d; +}; + +void to_json(nlohmann::json& j, const Holder& h); // triggers the error +``` + +To decide whether `Dummy` is copyable, the compiler checks whether a `Dummy` can be converted to `json`. That check +looks up `to_json` via argument-dependent lookup, finds the unrelated `to_json` for `Holder`, and eventually asks again +whether `Dummy` is copyable. GCC before version 11 turns this cycle into a hard error; GCC 11 and later, Clang, and +C++17 mode compile the code. The same error shows up without this library whenever a constrained converting constructor +is involved, so the library can't avoid it. + +To work around this, define `to_json` (and `from_json`) as a *hidden friend* inside the class. That way, +argument-dependent lookup only finds it for `Holder`: + +```cpp +class Holder { + boost::optional d; + + friend void to_json(nlohmann::json& j, const Holder& h) { /* ... */ } +}; +``` + +The [`NLOHMANN_DEFINE_TYPE_INTRUSIVE`](../api/macros/nlohmann_define_type_intrusive.md) macros define hidden friends as +well. See [#3669](https://github.com/nlohmann/json/issues/3669) for details. + ### Missing STL function !!! question "Questions" diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 281c160af..1ea5ac595 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -233,6 +233,52 @@ class my_allocator : public std::allocator }; }; +///////////////////////////////////////////////////////////////////// +// for #3669 +///////////////////////////////////////////////////////////////////// + +// mimics boost::optional's converting constructor, whose SFINAE check asks +// whether T is constructible from const U& +template +struct issue3669_is_constructible +{ + template()))> + static char test(int); + template + static long test(...); + static constexpr bool value = sizeof(test(0)) == 1; +}; + +template +class issue3669_optional +{ + public: + issue3669_optional() = default; + template + issue3669_optional(const issue3669_optional& /*unused*/, // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + typename std::enable_if::value, bool>::type /*unused*/ = true) {} +}; + +class Issue3669Dummy +{ + public: + explicit Issue3669Dummy(const json& /*unused*/) {} +}; + +class Issue3669Holder +{ + issue3669_optional d{}; + + // GCC < 11 (C++11/14) rejects a free to_json(json&, const Issue3669Holder&) + // here, because ADL for Issue3669Dummy finds it and closes an instantiation + // cycle; a hidden friend is only visible to ADL for Issue3669Holder + friend void to_json(json& j, const Issue3669Holder& h) + { + static_cast(h.d); // silence -Wunused-private-field + j = "holder"; + } +}; + TEST_CASE("regression tests 2") { SECTION("issue #1001 - Fix memory leak during parser callback") @@ -762,6 +808,14 @@ TEST_CASE("regression tests 2") CHECK(j == k); } + SECTION("issue #3669 - invalid use of incomplete type with optional member and to_json") + { + const Issue3669Holder h{}; + const Issue3669Holder h2(h); // NOLINT(performance-unnecessary-copy-initialization) + const json j = h2; + CHECK(j == "holder"); + } + } TEST_CASE("regression test - parser callback must not lose a duplicate key's prior value") From d11e89471bdf0223b4a97fbfb25d95e36c8cd91a Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:18 +0200 Subject: [PATCH 03/12] Assert on missing array indices in const operator[] and document the JSON pointer case (#5606) The const operator[] overloads are unchecked by design, and a missing key or index is undefined behavior. The key overload guards this with a runtime assertion, but the index overload did not, although the element access documentation says an assertion fires in both cases. The const JSON pointer overload inherits both through json_pointer::get_unchecked(), so a pointer to a missing array index read out of bounds even in debug builds, and its documentation promised out_of_range.404 for any pointer that cannot be resolved. Add JSON_ASSERT(idx < size()) to const operator[](size_type), which also covers the index leg of the const JSON pointer overload. Document the undefined behavior for the const JSON pointer overload in operator[].md and in the runtime assertions page. Release builds are unchanged. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/operator[].md | 14 +++++-- docs/mkdocs/docs/features/assertions.md | 38 +++++++++++++++---- include/nlohmann/detail/json_pointer.hpp | 10 ++++- include/nlohmann/json.hpp | 1 + single_include/nlohmann/json.hpp | 11 +++++- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/operator[].md b/docs/mkdocs/docs/api/basic_json/operator[].md index 23926d5f6..74996d629 100644 --- a/docs/mkdocs/docs/api/basic_json/operator[].md +++ b/docs/mkdocs/docs/api/basic_json/operator[].md @@ -89,6 +89,9 @@ Strong exception safety: if an exception occurs, the original value stays intact - Throws [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) if an array index in the passed JSON pointer `ptr` exceeds the range of `size_type` (e.g., on 32-bit platforms). + For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an + exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access. + ## Complexity 1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`. @@ -103,9 +106,12 @@ Strong exception safety: if an exception occurs, the original value stays intact The following cases apply to the **const** overloads; the non-const overloads instead insert the missing element (see the notes below). - 1. If the element at index `idx` does not exist, the behavior is undefined. + 1. If the element at index `idx` does not exist, the behavior is undefined and is **guarded by a + [runtime assertion](../../features/assertions.md)**! 2. If the element with key `key` does not exist, the behavior is undefined and is **guarded by a [runtime assertion](../../features/assertions.md)**! + 3. If the JSON pointer `ptr` refers to an object key or an array index that does not exist, the behavior is + undefined and is **guarded by a [runtime assertion](../../features/assertions.md)**! 1. The non-const version may add values: If `idx` is beyond the range of the array (i.e., `idx >= size()`), then the array is silently filled up with `#!json null` values to make `idx` a valid reference to the last stored element. In @@ -273,9 +279,11 @@ Strong exception safety: if an exception occurs, the original value stays intact ## Version history 1. Added in version 1.0.0. Fixed in version 3.13.0 to throw `#!cpp std::length_error` instead of emptying the array and - accessing it out of bounds when `idx` equals the maximum value of `size_type`. + accessing it out of bounds when `idx` equals the maximum value of `size_type`. A missing index in the const version + is guarded by a runtime assertion since version 3.13.0. 2. Added in version 1.0.0. Added overloads for `T* key` in version 1.1.0. Removed overloads for `T* key` (replaced by 3) in version 3.11.0. 3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as already supported by [`at`](at.md), [`value`](value.md), [`find`](find.md), and other lookup functions. -4. Added in version 2.0.0. +4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since + version 3.13.0. diff --git a/docs/mkdocs/docs/features/assertions.md b/docs/mkdocs/docs/features/assertions.md index e0b850115..dadb0bd87 100644 --- a/docs/mkdocs/docs/features/assertions.md +++ b/docs/mkdocs/docs/features/assertions.md @@ -16,14 +16,15 @@ before including the `json.hpp` header. ## Function with runtime assertions -### Unchecked object access to a const value +### Unchecked access to a const value -Function [`operator[]`](../api/basic_json/operator%5B%5D.md) implements unchecked access for objects. Whereas a missing -key is added in the case of non-const objects, accessing a const object with a missing key is undefined behavior (think -of a dereferenced null pointer) and yields a runtime assertion. +Function [`operator[]`](../api/basic_json/operator%5B%5D.md) implements unchecked access for arrays and objects. Whereas +a missing element is added in the case of non-const values, accessing a const value with a missing object key or an +invalid array index is undefined behavior (think of a dereferenced null pointer) and yields a runtime assertion. This +also applies to a [JSON pointer](json_pointer.md) that refers to a missing key or an invalid index. -If you are not sure whether an element in an object exists, use checked access with the -[`at` function](../api/basic_json/at.md) or call the [`contains` function](../api/basic_json/contains.md) before. +If you are not sure whether an element exists, use checked access with the [`at` function](../api/basic_json/at.md) +or call the [`contains` function](../api/basic_json/contains.md) before. See also the documentation on [element access](element_access/index.md). @@ -46,7 +47,30 @@ See also the documentation on [element access](element_access/index.md). Output: ``` - Assertion failed: (m_value.object->find(key) != m_value.object->end()), function operator[], file json.hpp, line 2144. + Assertion failed: (it != m_data.m_value.object->end()), function operator[], file json.hpp, line 28795. + ``` + +??? example "Example 2: Invalid array index in a JSON pointer" + + The following code will trigger an assertion at runtime: + + ```cpp + #include + + using json = nlohmann::json; + using namespace nlohmann::literals; + + int main() + { + const json j = {{"array", {1, 2, 3}}}; + auto v = j["/array/5"_json_pointer]; + } + ``` + + Output: + + ``` + Assertion failed: (idx < m_data.m_value.array->size()), function operator[], file json.hpp, line 28758. ``` ### Constructing from an uninitialized iterator range diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index cf3720a25..e34acb6d3 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -536,6 +536,10 @@ class json_pointer @return const reference to the JSON value pointed to by the JSON pointer + @pre Every object key and array index the pointer refers to exists. + Like the const operator[] for keys and indices, a missing one is + undefined behavior, guarded by a runtime assertion. + @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if an array index was not a number @throw out_of_range.402 if the array index '-' is used @@ -550,7 +554,8 @@ class json_pointer { case detail::value_t::object: { - // use unchecked object access + // use unchecked object access; the const operator[] + // asserts that the key exists ptr = &ptr->operator[](reference_token); break; } @@ -563,7 +568,8 @@ class json_pointer JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); } - // use unchecked array access + // use unchecked array access; the const operator[] + // asserts that the index exists ptr = &ptr->operator[](array_index(reference_token)); break; } diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index bb78b211d..1ff12c7f3 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -3065,6 +3065,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for arrays if (JSON_HEDLEY_LIKELY(is_array())) { + JSON_ASSERT(idx < m_data.m_value.array->size()); return m_data.m_value.array->operator[](idx); } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index c4f3313ae..9e27f62f0 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20105,6 +20105,10 @@ class json_pointer @return const reference to the JSON value pointed to by the JSON pointer + @pre Every object key and array index the pointer refers to exists. + Like the const operator[] for keys and indices, a missing one is + undefined behavior, guarded by a runtime assertion. + @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if an array index was not a number @throw out_of_range.402 if the array index '-' is used @@ -20119,7 +20123,8 @@ class json_pointer { case detail::value_t::object: { - // use unchecked object access + // use unchecked object access; the const operator[] + // asserts that the key exists ptr = &ptr->operator[](reference_token); break; } @@ -20132,7 +20137,8 @@ class json_pointer JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); } - // use unchecked array access + // use unchecked array access; the const operator[] + // asserts that the index exists ptr = &ptr->operator[](array_index(reference_token)); break; } @@ -29764,6 +29770,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for arrays if (JSON_HEDLEY_LIKELY(is_array())) { + JSON_ASSERT(idx < m_data.m_value.array->size()); return m_data.m_value.array->operator[](idx); } From df27cc3d4cdf9ecb9337524c2af7752967ebf4ca Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:21 +0200 Subject: [PATCH 04/12] Add scalar-on-left overloads for legacy discarded comparisons in C++20 (#5682) With JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1 and C++20, a scalar on the left-hand side of <= or >= (e.g., `1 <= discarded`) yielded false instead of the documented true. The C++20 legacy block only had member operators, which are only candidates when the basic_json is the left operand; for a scalar on the left, overload resolution picked the candidate rewritten from operator<=>, which does not emulate the legacy behavior. The C++17 branch already has scalar-on-the-left friend overloads for <= and >=; add the equivalent pair to the C++20 legacy block. Added a regression test to tests/src/unit-comparison.cpp covering all four operand orders for both operators. Fixes #5665. Signed-off-by: Niels Lohmann --- ...n_use_legacy_discarded_value_comparison.md | 3 +++ include/nlohmann/json.hpp | 21 ++++++++++++++++++ single_include/nlohmann/json.hpp | 21 ++++++++++++++++++ tests/src/unit-comparison.cpp | 22 +++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/docs/mkdocs/docs/api/macros/json_use_legacy_discarded_value_comparison.md b/docs/mkdocs/docs/api/macros/json_use_legacy_discarded_value_comparison.md index b6efd8dbd..e61ce1a3e 100644 --- a/docs/mkdocs/docs/api/macros/json_use_legacy_discarded_value_comparison.md +++ b/docs/mkdocs/docs/api/macros/json_use_legacy_discarded_value_comparison.md @@ -81,3 +81,6 @@ When the macro is not defined, the library will define it to its default value. ## Version history - Added in version 3.11.0. +- Fixed in version 3.13.0 so `<=` and `>=` also emulate the legacy behavior in C++20 when the JSON value is the + right-hand operand of a scalar comparison; before, only the 3-way-comparison-rewritten candidate was found, which + yielded `#!cpp false` instead of `#!cpp true`. diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 1ff12c7f3..b802b4b59 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -4939,6 +4939,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { return *this >= basic_json(rhs); } + + // a scalar on the left-hand side would otherwise select the candidate + // rewritten from operator<=>, which does not emulate the legacy behavior + + /// @brief comparison: less than or equal + /// @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 + { + return basic_json(lhs) <= rhs; + } + + /// @brief comparison: greater than or equal + /// @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 + { + return basic_json(lhs) >= rhs; + } #endif #else /// @brief comparison: equal diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 9e27f62f0..2adc9978f 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -31644,6 +31644,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { return *this >= basic_json(rhs); } + + // a scalar on the left-hand side would otherwise select the candidate + // rewritten from operator<=>, which does not emulate the legacy behavior + + /// @brief comparison: less than or equal + /// @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 + { + return basic_json(lhs) <= rhs; + } + + /// @brief comparison: greater than or equal + /// @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 + { + return basic_json(lhs) >= rhs; + } #endif #else /// @brief comparison: equal diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index febfd9b42..a65d5c1b3 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -750,6 +750,28 @@ TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P24 CHECK_FALSE(j != i); } } + +#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON +TEST_CASE("regression #5665 - scalar <= discarded and scalar >= discarded in C++20 legacy mode") +{ + // Issue #5665: with a scalar on the left-hand side, <= and >= only had the + // candidate rewritten from operator<=>, which does not emulate the legacy + // discarded-value behavior. Check that scalar-on-the-left now matches the + // other three operand orders. + const json discarded(json::value_t::discarded); + const json one = 1; + + CHECK(discarded <= 1); + CHECK(discarded >= 1); + CHECK(one <= discarded); + CHECK(one >= discarded); + CHECK(1 <= discarded); + CHECK(1 >= discarded); + CHECK(1.5 <= discarded); + CHECK(1.5 >= discarded); +} +#endif + #endif namespace From 6f2048cd5d609c055742d91539457567a463cb4d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:25 +0200 Subject: [PATCH 05/12] Accept lvalues in ordered_map::emplace's value parameter (#5685) * Accept lvalues in ordered_map::emplace's value parameter ordered_map::emplace(key, value) took the mapped value only by T&&, an rvalue reference rather than a forwarding reference, so ordered_json::emplace("a", value) failed to compile whenever value was an lvalue or a const lvalue, even though the same call compiles for json (whose object_t is std::map, with a variadic emplace). Turn the value parameter into a separately-deduced forwarding reference, constrained with std::is_constructible so the overloads still only accept something convertible to the mapped type. std::map-compatible semantics are unchanged: emplace still does nothing if the key already exists. Open PR #5609 also touches ordered_map.hpp (moving values on vector growth); this change only touches the two emplace() overloads and should not conflict. Fixes #5673. Signed-off-by: Niels Lohmann * Avoid astyle's padding in ordered_map::emplace's template headers Use detail::conjunction instead of && and drop the redundant V&& in detail::is_constructible, so astyle keeps the usual template formatting. Addresses review comment by @gregmarr. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/emplace.md | 2 + include/nlohmann/ordered_map.hpp | 15 +++-- single_include/nlohmann/json.hpp | 15 +++-- tests/src/unit-ordered_json.cpp | 41 ++++++++++++ tests/src/unit-ordered_map.cpp | 73 ++++++++++++++++++++++ 5 files changed, 134 insertions(+), 12 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/emplace.md b/docs/mkdocs/docs/api/basic_json/emplace.md index 26044a597..09953d347 100644 --- a/docs/mkdocs/docs/api/basic_json/emplace.md +++ b/docs/mkdocs/docs/api/basic_json/emplace.md @@ -70,3 +70,5 @@ Logarithmic in the size of the container, O(log(`size()`)). ## Version history - Since version 2.0.8. +- Fixed in version 3.13.0: for [`ordered_json`](../ordered_json.md), the value could previously only be passed as an + rvalue; it can now also be passed as an lvalue or a `#!cpp const` lvalue, matching the behavior of `json`. diff --git a/include/nlohmann/ordered_map.hpp b/include/nlohmann/ordered_map.hpp index 656f24264..d1c247483 100644 --- a/include/nlohmann/ordered_map.hpp +++ b/include/nlohmann/ordered_map.hpp @@ -74,7 +74,9 @@ template , return *this; } - std::pair emplace(const key_type& key, T&& t) + template::value, int> = 0> + std::pair emplace(const key_type& key, V && t) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -83,13 +85,14 @@ template , return {it, false}; } } - append(key, std::forward(t)); + append(key, std::forward(t)); return {std::prev(this->end()), true}; } - template::value, int> = 0> - std::pair emplace(KeyType && key, T && t) + template, + detail::is_constructible>::value, int> = 0> + std::pair emplace(KeyType && key, V && t) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -98,7 +101,7 @@ template , return {it, false}; } } - append(std::forward(key), std::forward(t)); + append(std::forward(key), std::forward(t)); return {std::prev(this->end()), true}; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 2adc9978f..fe3547ab2 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -26401,7 +26401,9 @@ template , return *this; } - std::pair emplace(const key_type& key, T&& t) + template::value, int> = 0> + std::pair emplace(const key_type& key, V && t) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -26410,13 +26412,14 @@ template , return {it, false}; } } - append(key, std::forward(t)); + append(key, std::forward(t)); return {std::prev(this->end()), true}; } - template::value, int> = 0> - std::pair emplace(KeyType && key, T && t) + template, + detail::is_constructible>::value, int> = 0> + std::pair emplace(KeyType && key, V && t) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -26425,7 +26428,7 @@ template , return {it, false}; } } - append(std::forward(key), std::forward(t)); + append(std::forward(key), std::forward(t)); return {std::prev(this->end()), true}; } diff --git a/tests/src/unit-ordered_json.cpp b/tests/src/unit-ordered_json.cpp index 45fbf5493..135dedade 100644 --- a/tests/src/unit-ordered_json.cpp +++ b/tests/src/unit-ordered_json.cpp @@ -196,3 +196,44 @@ TEST_CASE("regression test - diff() must account for ordered_json member order") CHECK(a.patch(p) == b); } } + +TEST_CASE("regression test for issue #5673 - ordered_json::emplace with a non-rvalue value") +{ + SECTION("lvalue value") + { + ordered_json oj = ordered_json::object(); + ordered_json value = 1; + auto res = oj.emplace("a", value); + CHECK(res.second == true); + CHECK(oj.dump() == "{\"a\":1}"); + } + + SECTION("const lvalue value") + { + ordered_json oj = ordered_json::object(); + const ordered_json value = 1; + auto res = oj.emplace("a", value); + CHECK(res.second == true); + CHECK(oj.dump() == "{\"a\":1}"); + } + + SECTION("rvalue value") + { + ordered_json oj = ordered_json::object(); + auto res = oj.emplace("a", ordered_json(1)); + CHECK(res.second == true); + CHECK(oj.dump() == "{\"a\":1}"); + } + + SECTION("existing key is not overwritten (std::map-compatible semantics)") + { + ordered_json oj = ordered_json::object(); + ordered_json value = 1; + oj.emplace("a", value); + + ordered_json other_value = 2; + auto res = oj.emplace("a", other_value); + CHECK(res.second == false); + CHECK(oj.dump() == "{\"a\":1}"); + } +} diff --git a/tests/src/unit-ordered_map.cpp b/tests/src/unit-ordered_map.cpp index 98b6fa0d1..dce3f61a5 100644 --- a/tests/src/unit-ordered_map.cpp +++ b/tests/src/unit-ordered_map.cpp @@ -403,6 +403,79 @@ TEST_CASE("ordered_map") CHECK(om.size() == 4); } } + + SECTION("emplace") + { + // regression test for issue #5673: the mapped-value parameter must + // accept lvalues and const lvalues, not just rvalues + ordered_map om; + om["eins"] = "one"; + om["zwei"] = "two"; + om["drei"] = "three"; + + SECTION("with T&& (rvalue)") + { + auto res1 = om.emplace("eins", std::string("1")); + CHECK(res1.first == om.begin()); + CHECK(res1.second == false); + CHECK(om.size() == 3); + CHECK(om.at("eins") == "one"); // existing key is not overwritten + + auto res4 = om.emplace("vier", std::string("four")); + CHECK(res4.first == om.begin() + 3); + CHECK(res4.second == true); + CHECK(om.size() == 4); + CHECK(om.at("vier") == "four"); + } + + SECTION("with T& (lvalue)") + { + std::string one = "1"; + std::string four = "four"; + + auto res1 = om.emplace("eins", one); + CHECK(res1.first == om.begin()); + CHECK(res1.second == false); + CHECK(om.size() == 3); + CHECK(om.at("eins") == "one"); // existing key is not overwritten + + auto res4 = om.emplace("vier", four); + CHECK(res4.first == om.begin() + 3); + CHECK(res4.second == true); + CHECK(om.size() == 4); + CHECK(om.at("vier") == "four"); + CHECK(four == "four"); // source was copied, not moved from + } + + SECTION("with const T&") + { + const std::string one = "1"; + const std::string four = "four"; + + auto res1 = om.emplace("eins", one); + CHECK(res1.first == om.begin()); + CHECK(res1.second == false); + CHECK(om.size() == 3); + + auto res4 = om.emplace("vier", four); + CHECK(res4.first == om.begin() + 3); + CHECK(res4.second == true); + CHECK(om.size() == 4); + CHECK(om.at("vier") == "four"); + } + + SECTION("with key of key_type (non-template overload)") + { + const std::string key_vier{"vier"}; + std::string four = "four"; + + auto res4 = om.emplace(key_vier, four); + CHECK(res4.first == om.begin() + 3); + CHECK(res4.second == true); + CHECK(om.size() == 4); + CHECK(om.at("vier") == "four"); + } + } } TEST_CASE("ordered_map growth") From 0d01d6ae9074e167bcdca0bc6e592c02e900c692 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:29 +0200 Subject: [PATCH 06/12] Classify leaves with operator<=> itself past the nesting bound (#5686) * Classify leaves with operator<=> itself past the nesting bound In C++20, an ordered comparison past the nesting bound classified a pair of leaves by asking == first and then order_leaves(), which calls < and > - both derived from <=>. For a pair of binary values with the same bytes but a different subtype, == reports them unequal, while <=> (through std::vector::operator<=>) reports them equivalent, so the pair ended the comparison as unordered instead of letting the next element decide - unlike an array or object within the bound, which compares such a pair with its own operator<=> and gets equivalent. So operator<=>, and the <, <=, >, >= derived from it, could give a different result for the same two values depending on how deeply the values were nested, or unordered at every depth with JSON_NO_THREAD_LOCAL defined. compare_leaves() now classifies such a pair in C++20 with operator<=> itself instead, matching how a value within the bound is compared; the equality-only and pre-C++20 ordered cases are unchanged. Which of the three runs is chosen by overloading on std::integral_constant, the same tag dispatch order_leaves() already uses, rather than a runtime "if (Ordered)" on a template parameter, which MSVC would flag as a constant condition (C4127). Added a regression test to unit-comparison.cpp that nests such a pair 0, 127, 128 and 200 levels deep (127 stays within the 128-level bound, 128 and 200 do not) and checks that operator<=> and operator< agree at every depth. Fixes #5654. Signed-off-by: Niels Lohmann * Drop the version history note for a bug that was never released The regression came from #5390, which is not in any release. Addresses review comment by @gregmarr. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- include/nlohmann/json.hpp | 52 +++++++++++++++++++++++++++++++- single_include/nlohmann/json.hpp | 52 +++++++++++++++++++++++++++++++- tests/src/unit-comparison.cpp | 48 +++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index b802b4b59..bf3052748 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1528,15 +1528,65 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec */ template static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept + { + return compare_leaves(lhs, rhs, std::integral_constant {}); + } + + /// @brief compare two leaves that are only being checked for equality + static compare_result compare_leaves(const_reference lhs, const_reference rhs, std::false_type /*ordered*/) noexcept { if (lhs == rhs) { return compare_result::equal; } - return order_leaves(lhs, rhs, std::integral_constant {}); + return order_leaves(lhs, rhs, std::false_type {}); } +#if JSON_HAS_THREE_WAY_COMPARISON + /*! + @brief compare two leaves that are being ordered, for operator<=> + + Reached only from operator<=>, so the leaves must be classified exactly + as operator<=> classifies them - which is not the same as asking + == and then order_leaves(), the way the other overload does it. The two + disagree on a binary value: == also compares the subtype, but <=> compares + only the bytes, through std::vector::operator<=>. Using <=> + itself here keeps a leaf pair classified the same way regardless of how + deep it is nested - == first would again call operator<=> a level down + through order_leaves(), but call it after a mismatching == already ended + the comparison for a pair that <=> alone would still call equivalent. + */ + static compare_result compare_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept + { + const std::partial_ordering order = lhs <=> rhs; // *NOPAD* + if (order == 0) + { + return compare_result::equal; + } + if (order < 0) + { + return compare_result::less; + } + if (order > 0) + { + return compare_result::greater; + } + return compare_result::unordered; + } +#else + /// @brief compare two leaves that are being ordered, for operator< + static compare_result compare_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept + { + if (lhs == rhs) + { + return compare_result::equal; + } + + return order_leaves(lhs, rhs, std::true_type {}); + } +#endif + /*! @brief compare two object keys diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index fe3547ab2..3c1048003 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -28236,15 +28236,65 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec */ template static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept + { + return compare_leaves(lhs, rhs, std::integral_constant {}); + } + + /// @brief compare two leaves that are only being checked for equality + static compare_result compare_leaves(const_reference lhs, const_reference rhs, std::false_type /*ordered*/) noexcept { if (lhs == rhs) { return compare_result::equal; } - return order_leaves(lhs, rhs, std::integral_constant {}); + return order_leaves(lhs, rhs, std::false_type {}); } +#if JSON_HAS_THREE_WAY_COMPARISON + /*! + @brief compare two leaves that are being ordered, for operator<=> + + Reached only from operator<=>, so the leaves must be classified exactly + as operator<=> classifies them - which is not the same as asking + == and then order_leaves(), the way the other overload does it. The two + disagree on a binary value: == also compares the subtype, but <=> compares + only the bytes, through std::vector::operator<=>. Using <=> + itself here keeps a leaf pair classified the same way regardless of how + deep it is nested - == first would again call operator<=> a level down + through order_leaves(), but call it after a mismatching == already ended + the comparison for a pair that <=> alone would still call equivalent. + */ + static compare_result compare_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept + { + const std::partial_ordering order = lhs <=> rhs; // *NOPAD* + if (order == 0) + { + return compare_result::equal; + } + if (order < 0) + { + return compare_result::less; + } + if (order > 0) + { + return compare_result::greater; + } + return compare_result::unordered; + } +#else + /// @brief compare two leaves that are being ordered, for operator< + static compare_result compare_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept + { + if (lhs == rhs) + { + return compare_result::equal; + } + + return order_leaves(lhs, rhs, std::true_type {}); + } +#endif + /*! @brief compare two object keys diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index a65d5c1b3..4b66f1d07 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -1020,3 +1020,51 @@ TEST_CASE("containers are compared element by element") } } } + +#if JSON_HAS_THREE_WAY_COMPARISON +// JSON_HAS_CPP_20 (do not remove; see note at top of file) +TEST_CASE("operator<=> of binary values with a different subtype does not depend on nesting depth") +{ + // #5654: std::vector::operator<=>, which the binary type's + // own operator<=> uses, ignores the subtype that operator== checks. So a + // pair of binary values with the same bytes but a different subtype is + // unequal, yet <=>-equivalent - the same inconsistency between == and <=> + // that a NaN has. Within the nesting bound, an array compares itself + // with std::vector's own operator<=>, which treats an equivalent pair as + // undecided and lets the next element decide, same as + // std::lexicographical_compare_three_way does. Past the bound, + // compare_iteratively() takes over and must classify the pair the + // same way, or the result of operator<=> - and of <, which C++20 derives + // from it - depends on how deeply the values are nested. + const json a = json::array({json::binary({1}, 1), 1}); + const json b = json::array({json::binary({1}, 2), 2}); + + // the root inconsistency: unequal, yet <=>-equivalent + CHECK_FALSE(a[0] == b[0]); + CHECK((a[0] <=> b[0]) == std::partial_ordering::equivalent); // *NOPAD* + + const auto deep = [](const json & j, const std::size_t depth) + { + json result = j; + for (std::size_t i = 0; i < depth; ++i) + { + result = json::array({std::move(result)}); + } + return result; + }; + + // 127 levels stay within nesting_depth_limit() (128); 128 and 200 do not, + // and must still agree with the levels that do + for (const std::size_t depth : std::vector {0, 127, 128, 200}) + { + CAPTURE(depth); + const json x = deep(a, depth); + const json y = deep(b, depth); + CHECK((x <=> y) == std::partial_ordering::less); // *NOPAD* + CHECK((y <=> x) == std::partial_ordering::greater); // *NOPAD* + CHECK(x < y); + CHECK(y > x); + CHECK_FALSE(y < x); + } +} +#endif From b730946432dd9b22ecd012515f50a191d96954fc Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:32 +0200 Subject: [PATCH 07/12] Fix key types convertible to std::string_view breaking lookups (#5689) Since #4958, a key type implicitly convertible to std::string_view was accepted by is_usable_as_basic_json_key_type without checking that the object's comparator can actually compare object_t::key_type with that key type. The key was then forwarded unchanged to the underlying map, so const operator[], at, find, count, contains, erase and value failed to compile (a hard error inside ) for a key convertible only to std::string_view, and value() rejected such keys outright. For keys convertible to both std::string and std::string_view, the KeyType&& templates now won overload resolution over the object_t::key_type overloads and then failed the same way, a regression from 3.12.0. Only the non-const operator[] worked, because it uses emplace(), which constructs a std::string from the key explicitly. ordered_json was not affected, since ordered_map checks comparability itself. Add a trait, is_string_view_convertible_key_type, that recognizes a key type that is convertible to std::string_view but not directly comparable with the object's key type, provided std::string_view itself is comparable with it. at(), operator[], find(), count(), contains(), erase() and value() now route such keys through a new lookup_key() helper that converts them to std::string_view before they reach the object, matching how the object's transparent comparator already supports std::string_view lookups. Fixes #5663. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/at.md | 4 +- docs/mkdocs/docs/api/basic_json/contains.md | 4 +- docs/mkdocs/docs/api/basic_json/count.md | 4 +- docs/mkdocs/docs/api/basic_json/erase.md | 4 +- docs/mkdocs/docs/api/basic_json/find.md | 4 +- docs/mkdocs/docs/api/basic_json/value.md | 4 +- include/nlohmann/detail/meta/type_traits.hpp | 28 ++++- include/nlohmann/json.hpp | 44 ++++++-- single_include/nlohmann/json.hpp | 72 +++++++++--- tests/src/unit-element_access2.cpp | 111 +++++++++++++++++++ 10 files changed, 243 insertions(+), 36 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/at.md b/docs/mkdocs/docs/api/basic_json/at.md index 60daf38e3..18f108964 100644 --- a/docs/mkdocs/docs/api/basic_json/at.md +++ b/docs/mkdocs/docs/api/basic_json/at.md @@ -238,5 +238,7 @@ Strong exception safety: if an exception occurs, the original value stays intact 1. Added in version 1.0.0. 2. Added in version 1.0.0. -3. Added in version 3.11.0. +3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as + already supported by [`operator[]`](operator[].md), [`value`](value.md), [`find`](find.md), and other lookup + functions. 4. Added in version 2.0.0. diff --git a/docs/mkdocs/docs/api/basic_json/contains.md b/docs/mkdocs/docs/api/basic_json/contains.md index 73bcf6f0c..63ec87a1d 100644 --- a/docs/mkdocs/docs/api/basic_json/contains.md +++ b/docs/mkdocs/docs/api/basic_json/contains.md @@ -131,7 +131,9 @@ Logarithmic in the size of the JSON object. ## Version history 1. Added in version 3.11.0. -2. Added in version 3.6.0. Extended template `KeyType` to support comparable types in version 3.11.0. +2. Added in version 3.6.0. Extended template `KeyType` to support comparable types in version 3.11.0. Fixed in + version 3.13.0 to consistently accept `std::string_view`-convertible keys, as already supported by + [`operator[]`](operator[].md), [`at`](at.md), [`value`](value.md), and other lookup functions. 3. Added in version 3.7.0. 4. Deleted overloads for integral key types added in version 3.13.0 to reject such calls at compile time instead of causing undefined behavior at runtime. diff --git a/docs/mkdocs/docs/api/basic_json/count.md b/docs/mkdocs/docs/api/basic_json/count.md index bffc46534..14b707525 100644 --- a/docs/mkdocs/docs/api/basic_json/count.md +++ b/docs/mkdocs/docs/api/basic_json/count.md @@ -84,6 +84,8 @@ Logarithmic in the size of the JSON object. ## Version history 1. Added in version 3.11.0. -2. Added in version 1.0.0. Changed parameter `key` type to `KeyType&&` in version 3.11.0. +2. Added in version 1.0.0. Changed parameter `key` type to `KeyType&&` in version 3.11.0. Fixed in version 3.13.0 to + consistently accept `std::string_view`-convertible keys, as already supported by [`operator[]`](operator[].md), + [`at`](at.md), [`value`](value.md), and other lookup functions. 3. Deleted overload for integral key types added in version 3.13.0 to reject such calls at compile time instead of causing undefined behavior at runtime. diff --git a/docs/mkdocs/docs/api/basic_json/erase.md b/docs/mkdocs/docs/api/basic_json/erase.md index d1e6d6d22..47531fed8 100644 --- a/docs/mkdocs/docs/api/basic_json/erase.md +++ b/docs/mkdocs/docs/api/basic_json/erase.md @@ -213,5 +213,7 @@ Strong exception safety: if an exception occurs, the original value stays intact 1. Added in version 1.0.0. Added support for binary types in version 3.8.0. 2. Added in version 1.0.0. Added support for binary types in version 3.8.0. 3. Added in version 1.0.0. -4. Added in version 3.11.0. +4. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as + already supported by [`operator[]`](operator[].md), [`at`](at.md), [`value`](value.md), and other lookup + functions. 5. Added in version 1.0.0. diff --git a/docs/mkdocs/docs/api/basic_json/find.md b/docs/mkdocs/docs/api/basic_json/find.md index bc746ee2f..59c2eab68 100644 --- a/docs/mkdocs/docs/api/basic_json/find.md +++ b/docs/mkdocs/docs/api/basic_json/find.md @@ -88,6 +88,8 @@ Logarithmic in the size of the JSON object. ## Version history 1. Added in version 3.11.0. -2. Added in version 1.0.0. Changed to support comparable types in version 3.11.0. +2. Added in version 1.0.0. Changed to support comparable types in version 3.11.0. Fixed in version 3.13.0 to + consistently accept `std::string_view`-convertible keys, as already supported by [`operator[]`](operator[].md), + [`at`](at.md), [`value`](value.md), and other lookup functions. 3. Deleted overloads for integral key types added in version 3.13.0 to reject such calls at compile time instead of causing undefined behavior at runtime. diff --git a/docs/mkdocs/docs/api/basic_json/value.md b/docs/mkdocs/docs/api/basic_json/value.md index 56f4bcc0f..80f5691dc 100644 --- a/docs/mkdocs/docs/api/basic_json/value.md +++ b/docs/mkdocs/docs/api/basic_json/value.md @@ -222,7 +222,9 @@ changes to any JSON value. 1. Added in version 1.0.0. Changed parameter `default_value` type from `const ValueType&` to `ValueType&&` in version 3.11.0. Deleted overload for integral key types added in version 3.13.0 to reject such calls at compile time instead of causing undefined behavior at runtime. -2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2. +2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2. Fixed in version 3.13.0 + to consistently accept `std::string_view`-convertible keys, as already supported by + [`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), and other lookup functions. 3. Added in version 2.0.2. Extended to work with arrays in version 3.13.0, including fixing an issue where resolving `ptr` through an array unexpectedly threw `out_of_range` instead of returning the resolved element (or `default_value`, as documented). diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 96b70a774..2f837e046 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -760,6 +760,30 @@ using is_usable_as_key_type = typename std::conditional < std::true_type, std::false_type >::type; +#ifdef JSON_HAS_CPP_17 +// type trait to check if KeyType can only be used as an object key after +// converting it to std::string_view: it is convertible to std::string_view, the +// object's comparator cannot compare it with object_t::key_type directly, but +// can compare a std::string_view. JSON pointers and JSON iterators are ruled out +// first, so that the conversion checks are never instantiated for them (a JSON +// pointer's deprecated conversion to string_t would be named otherwise). +template < typename BasicJsonType, typename KeyTypeCVRef, typename KeyType = uncvref_t, + bool = is_json_pointer::value || is_json_iterator_of::value > +struct is_string_view_convertible_key_type : std::false_type {}; + +template +struct is_string_view_convertible_key_type + : std::integral_constant < bool, + std::is_convertible::value + && !is_usable_as_key_type::value + && is_usable_as_key_type::value > {}; +#else +template +struct is_string_view_convertible_key_type : std::false_type {}; +#endif + // type trait to check if KeyType can be used as an object key // true if: // - KeyType is comparable with BasicJsonType::object_t::key_type @@ -773,9 +797,7 @@ using is_usable_as_basic_json_key_type = typename std::conditional < typename BasicJsonType::object_t::key_type, KeyTypeCVRef, RequireTransparentComparator, ExcludeObjectKeyType>::value && !is_json_iterator_of::value) -#ifdef JSON_HAS_CPP_17 - || std::is_convertible::value -#endif + || is_string_view_convertible_key_type::value , std::true_type, std::false_type >::type; diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index bf3052748..f83c29480 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -813,6 +813,24 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return it; } + /// @brief the key to look up an object member with: the key itself, or its + /// std::string_view if the object can only be searched with that + template < typename KeyType, detail::enable_if_t < + !detail::is_string_view_convertible_key_type::value, int > = 0 > + static KeyType && lookup_key(KeyType && key) noexcept + { + return std::forward(key); + } + +#ifdef JSON_HAS_CPP_17 + template < typename KeyType, detail::enable_if_t < + detail::is_string_view_convertible_key_type::value, int > = 0 > + static std::string_view lookup_key(KeyType && key) + { + return std::forward(key); + } +#endif + /// @brief erase an element from the object and return the following one /// Not every map returns an iterator from erase(iterator): some containers /// (e.g., Abseil's hash maps) return void to avoid computing a successor @@ -3008,7 +3026,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); @@ -3046,7 +3064,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); @@ -3190,7 +3208,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto result = m_data.m_value.object->emplace(std::forward(key), nullptr); + auto result = m_data.m_value.object->emplace(lookup_key(std::forward(key)), nullptr); return set_parent(result.first->second); } @@ -3206,7 +3224,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); JSON_ASSERT(it != m_data.m_value.object->end()); return it->second; } @@ -3216,8 +3234,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec private: template - using is_comparable_with_object_key = detail::is_comparable < - object_comparator_t, const typename object_t::key_type&, KeyType >; + using is_comparable_with_object_key = std::integral_constant < bool, + detail::is_comparable < + object_comparator_t, const typename object_t::key_type&, KeyType >::value + || detail::is_string_view_convertible_key_type::value >; template using value_return_type = std::conditional < @@ -3604,7 +3624,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); } - const auto it = m_data.m_value.object->find(std::forward(key)); + const auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it != m_data.m_value.object->end()) { m_data.m_value.object->erase(it); @@ -3631,7 +3651,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::is_usable_as_basic_json_key_type::value, int> = 0> size_type erase(KeyType && key) { - return erase_internal(std::forward(key)); + return erase_internal(lookup_key(std::forward(key))); } /// @brief remove element from a JSON array given an index @@ -3711,7 +3731,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward(key))); } return result; @@ -3727,7 +3747,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward(key))); } return result; @@ -3750,7 +3770,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec size_type count(KeyType && key) const { // return 0 for all nonobject types - return is_object() ? m_data.m_value.object->count(std::forward(key)) : 0; + return is_object() ? m_data.m_value.object->count(lookup_key(std::forward(key))) : 0; } /// @brief check the existence of an element in a JSON object @@ -3768,7 +3788,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_WARN_UNUSED_RESULT bool contains(KeyType && key) const { - return is_object() && m_data.m_value.object->find(std::forward(key)) != m_data.m_value.object->end(); + return is_object() && m_data.m_value.object->find(lookup_key(std::forward(key))) != m_data.m_value.object->end(); } /// @brief check the existence of an element in a JSON object given a JSON pointer diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 3c1048003..689e233bd 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -4760,6 +4760,30 @@ using is_usable_as_key_type = typename std::conditional < std::true_type, std::false_type >::type; +#ifdef JSON_HAS_CPP_17 +// type trait to check if KeyType can only be used as an object key after +// converting it to std::string_view: it is convertible to std::string_view, the +// object's comparator cannot compare it with object_t::key_type directly, but +// can compare a std::string_view. JSON pointers and JSON iterators are ruled out +// first, so that the conversion checks are never instantiated for them (a JSON +// pointer's deprecated conversion to string_t would be named otherwise). +template < typename BasicJsonType, typename KeyTypeCVRef, typename KeyType = uncvref_t, + bool = is_json_pointer::value || is_json_iterator_of::value > +struct is_string_view_convertible_key_type : std::false_type {}; + +template +struct is_string_view_convertible_key_type + : std::integral_constant < bool, + std::is_convertible::value + && !is_usable_as_key_type::value + && is_usable_as_key_type::value > {}; +#else +template +struct is_string_view_convertible_key_type : std::false_type {}; +#endif + // type trait to check if KeyType can be used as an object key // true if: // - KeyType is comparable with BasicJsonType::object_t::key_type @@ -4773,9 +4797,7 @@ using is_usable_as_basic_json_key_type = typename std::conditional < typename BasicJsonType::object_t::key_type, KeyTypeCVRef, RequireTransparentComparator, ExcludeObjectKeyType>::value && !is_json_iterator_of::value) -#ifdef JSON_HAS_CPP_17 - || std::is_convertible::value -#endif + || is_string_view_convertible_key_type::value , std::true_type, std::false_type >::type; @@ -27521,6 +27543,24 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return it; } + /// @brief the key to look up an object member with: the key itself, or its + /// std::string_view if the object can only be searched with that + template < typename KeyType, detail::enable_if_t < + !detail::is_string_view_convertible_key_type::value, int > = 0 > + static KeyType && lookup_key(KeyType && key) noexcept + { + return std::forward(key); + } + +#ifdef JSON_HAS_CPP_17 + template < typename KeyType, detail::enable_if_t < + detail::is_string_view_convertible_key_type::value, int > = 0 > + static std::string_view lookup_key(KeyType && key) + { + return std::forward(key); + } +#endif + /// @brief erase an element from the object and return the following one /// Not every map returns an iterator from erase(iterator): some containers /// (e.g., Abseil's hash maps) return void to avoid computing a successor @@ -29716,7 +29756,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); @@ -29754,7 +29794,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); @@ -29898,7 +29938,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto result = m_data.m_value.object->emplace(std::forward(key), nullptr); + auto result = m_data.m_value.object->emplace(lookup_key(std::forward(key)), nullptr); return set_parent(result.first->second); } @@ -29914,7 +29954,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); JSON_ASSERT(it != m_data.m_value.object->end()); return it->second; } @@ -29924,8 +29964,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec private: template - using is_comparable_with_object_key = detail::is_comparable < - object_comparator_t, const typename object_t::key_type&, KeyType >; + using is_comparable_with_object_key = std::integral_constant < bool, + detail::is_comparable < + object_comparator_t, const typename object_t::key_type&, KeyType >::value + || detail::is_string_view_convertible_key_type::value >; template using value_return_type = std::conditional < @@ -30312,7 +30354,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); } - const auto it = m_data.m_value.object->find(std::forward(key)); + const auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it != m_data.m_value.object->end()) { m_data.m_value.object->erase(it); @@ -30339,7 +30381,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::is_usable_as_basic_json_key_type::value, int> = 0> size_type erase(KeyType && key) { - return erase_internal(std::forward(key)); + return erase_internal(lookup_key(std::forward(key))); } /// @brief remove element from a JSON array given an index @@ -30419,7 +30461,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward(key))); } return result; @@ -30435,7 +30477,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward(key))); } return result; @@ -30458,7 +30500,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec size_type count(KeyType && key) const { // return 0 for all nonobject types - return is_object() ? m_data.m_value.object->count(std::forward(key)) : 0; + return is_object() ? m_data.m_value.object->count(lookup_key(std::forward(key))) : 0; } /// @brief check the existence of an element in a JSON object @@ -30476,7 +30518,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_WARN_UNUSED_RESULT bool contains(KeyType && key) const { - return is_object() && m_data.m_value.object->find(std::forward(key)) != m_data.m_value.object->end(); + return is_object() && m_data.m_value.object->find(lookup_key(std::forward(key))) != m_data.m_value.object->end(); } /// @brief check the existence of an element in a JSON object given a JSON pointer diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index 04974c251..a64caff89 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -1973,4 +1973,115 @@ TEST_CASE("operator[] with user-defined std::string_view-convertible types") } } } + +TEST_CASE("keys convertible to std::string_view work with all lookup functions (regression test for #5663)") +{ + // a key type convertible only to std::string_view: the case #4958 added + // support for, but only the non-const operator[] compiled with it + struct ViewKey + { + operator std::string_view() const + { + return "a"; + } + }; + + // a key type convertible to both std::string and std::string_view: with + // 3.12.0, such a key worked with at, the const operator[], find, count and + // contains via the conversion to std::string; #4958 made the KeyType&& + // templates win overload resolution for it instead, and those then failed + struct DualKey + { + operator std::string() const + { + return "a"; + } + operator std::string_view() const + { + return "a"; + } + }; + + SECTION("nlohmann::json") + { + using json = nlohmann::json; + + SECTION("ViewKey") + { + json j = {{"a", 1}}; + const json& cj = j; + + CHECK(j[ViewKey{}] == 1); + CHECK(cj[ViewKey{}] == 1); + CHECK(j.at(ViewKey{}) == 1); + CHECK(cj.at(ViewKey{}) == 1); + CHECK(j.find(ViewKey{}) != j.end()); + CHECK(cj.find(ViewKey{}) != cj.end()); + CHECK(j.count(ViewKey{}) == 1); + CHECK(j.contains(ViewKey{})); + CHECK(j.value(ViewKey{}, 0) == 1); + CHECK(j.erase(ViewKey{}) == 1); + CHECK(!j.contains("a")); + } + + SECTION("DualKey") + { + json j = {{"a", 1}}; + const json& cj = j; + + CHECK(j[DualKey{}] == 1); + CHECK(cj[DualKey{}] == 1); + CHECK(j.at(DualKey{}) == 1); + CHECK(cj.at(DualKey{}) == 1); + CHECK(j.find(DualKey{}) != j.end()); + CHECK(cj.find(DualKey{}) != cj.end()); + CHECK(j.count(DualKey{}) == 1); + CHECK(j.contains(DualKey{})); + CHECK(j.value(DualKey{}, 0) == 1); + CHECK(j.erase(DualKey{}) == 1); + CHECK(!j.contains("a")); + } + } + + SECTION("nlohmann::ordered_json") + { + using ordered_json = nlohmann::ordered_json; + + SECTION("ViewKey") + { + ordered_json j = {{"a", 1}}; + const ordered_json& cj = j; + + CHECK(j[ViewKey{}] == 1); + CHECK(cj[ViewKey{}] == 1); + CHECK(j.at(ViewKey{}) == 1); + CHECK(cj.at(ViewKey{}) == 1); + CHECK(j.find(ViewKey{}) != j.end()); + CHECK(cj.find(ViewKey{}) != cj.end()); + CHECK(j.count(ViewKey{}) == 1); + CHECK(j.contains(ViewKey{})); + CHECK(j.value(ViewKey{}, 0) == 1); + CHECK(j.erase(ViewKey{}) == 1); + CHECK(!j.contains("a")); + } + + SECTION("DualKey") + { + ordered_json j = {{"a", 1}}; + const ordered_json& cj = j; + + CHECK(j[DualKey{}] == 1); + CHECK(cj[DualKey{}] == 1); + CHECK(j.at(DualKey{}) == 1); + CHECK(cj.at(DualKey{}) == 1); + CHECK(j.find(DualKey{}) != j.end()); + CHECK(cj.find(DualKey{}) != cj.end()); + CHECK(j.count(DualKey{}) == 1); + CHECK(j.contains(DualKey{})); + CHECK(j.value(DualKey{}, 0) == 1); + CHECK(j.erase(DualKey{}) == 1); + CHECK(!j.contains("a")); + } + } +} #endif From 49cd427196332935eb623fa448150425239d2c06 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:36 +0200 Subject: [PATCH 08/12] Copy-construct the base class of a deep copy's elements, not assign it (#5690) The bounded-descent copy added by #5389 built the elements of a deep copy (nested past the 128-level bound) by default-constructing them and then having copy_metadata() assign their base class afterwards. That assignment is only instantiated for values nested past the bound, but being called from copy_structured() at all meant it was compiled for every copy, so a CustomBaseClass that is copy-constructible but not move-assignable (for example one with a const data member) no longer let its basic_json be copy-constructed, at any depth. copy_array_level() and copy_object_level() now build each element with a private-tag-selected constructor that copy-constructs the base class (and, under JSON_DIAGNOSTIC_POSITIONS, copies the positions) directly, the same way the copy constructor already builds elements within the 128-level bound. Copying a basic_json is therefore back to requiring only a copy-constructible base class, as documented and as it was before #5389; copy assignment is unchanged and still requires an assignable one. Fixes #5674. Signed-off-by: Niels Lohmann --- include/nlohmann/json.hpp | 65 ++++++++++++++++++-------- single_include/nlohmann/json.hpp | 65 ++++++++++++++++++-------- tests/src/unit-custom-base-class.cpp | 70 ++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 38 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index f83c29480..268c28c42 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1028,19 +1028,39 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec using copy_scratch_value_t = std::pair; using copy_scratch_t = std::vector>; - /// @brief copy everything of @a src into @a dst but its type and value - static void copy_metadata(const basic_json& src, basic_json& dst) - { - // a custom base class is only required to be copy-constructible and - // move-assignable, so the copy has to go through a temporary - static_cast(dst) = json_base_class_t(static_cast(src)); + /// @brief tag selecting the constructor below; used only to build the + /// elements of a deep copy (@ref copy_array_level, @ref copy_object_level) + struct copy_construct_tag {}; + public: + /*! + @brief construct a null value whose base class - and, with @ref + JSON_DIAGNOSTIC_POSITIONS, positions - are copied from @a src + + Copy-constructing @ref json_base_class_t here, rather than default- + constructing the element and assigning its base class afterwards, means + that copying a @ref basic_json only ever requires a copy-constructible + base class, and never a move-assignable one as well. + + @note this constructor has to be public: @ref copy_array_level and + @ref copy_object_level reach it through @ref array_t's or @ref + object_t's own emplace_back(), which constructs the element from + outside @ref basic_json and so cannot call a private constructor. + @ref copy_construct_tag is private, though, and nothing in the + public interface hands out a value of it, so outside code can still + never name it to call this constructor itself. + */ + basic_json(copy_construct_tag /*unused*/, const basic_json& src) + : json_base_class_t(src) #if JSON_DIAGNOSTIC_POSITIONS - dst.start_position = src.start_position; - dst.end_position = src.end_position; + , start_position(src.start_position) + , end_position(src.end_position) #endif + { } + private: + /*! @brief copy the value of @a src into @a dst, which must not be structured @@ -1103,8 +1123,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } /*! - @brief copy everything of @a src into the null value @a dst but the children + @brief finish the copy @a dst of @a src that a @ref copy_construct_tag + constructor started, other than the children of an object or array + @a dst already has @a src's base class and, with @ref + JSON_DIAGNOSTIC_POSITIONS, positions; only its value is still missing. Objects and arrays are not copied here; they are appended to @a worklist to be created later by @ref copy_iteratively. Until that happens, @a dst remains a null value, so that a partially built copy can be destroyed at any point @@ -1112,8 +1135,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec */ static void copy_shallow(const basic_json& src, basic_json& dst, copy_worklist_t& worklist) { - copy_metadata(src, dst); - if (src.m_data.m_type == value_t::object || src.m_data.m_type == value_t::array) { // defer: dst stays a null value until its container exists @@ -1134,15 +1155,19 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { const array_t& src_array = *src.m_data.m_value.array; - // create all elements up front: growing the array afterwards could - // invalidate the pointers that are handed to the worklist; resize() - // rather than the fill constructor, because not every array type - // provides the latter (e.g., ones without a matching allocator-aware - // fill constructor) dst.m_data.m_value.array = create(); // only now that the array exists may dst stop being a null value dst.m_data.m_type = value_t::array; - dst.m_data.m_value.array->resize(src_array.size()); + + // create every element - its base class already copy-constructed from + // its counterpart in src, via the copy_construct_tag constructor - + // before any of their addresses are handed to worklist below: growing + // the array while that is going on could reallocate it and invalidate + // addresses taken from an earlier iteration + for (const auto& src_element : src_array) + { + dst.m_data.m_value.array->emplace_back(copy_construct_tag{}, src_element); + } auto dst_it = dst.m_data.m_value.array->begin(); for (auto src_it = src_array.cbegin(); src_it != src_array.cend(); ++src_it, ++dst_it) @@ -1160,12 +1185,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // build the complete key skeleton and hand it to the object's range // constructor: adding the keys one by one would be quadratic for object - // types that are backed by a vector, such as nlohmann::ordered_map + // types that are backed by a vector, such as nlohmann::ordered_map; each + // value's base class is already copy-constructed from its counterpart + // in src, via the copy_construct_tag constructor scratch.clear(); scratch.reserve(src_object.size()); for (const auto& element : src_object) { - scratch.emplace_back(element.first, basic_json()); + scratch.emplace_back(element.first, basic_json(copy_construct_tag{}, element.second)); } dst.m_data.m_value.object = create(std::make_move_iterator(scratch.begin()), diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 689e233bd..349977636 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -27758,19 +27758,39 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec using copy_scratch_value_t = std::pair; using copy_scratch_t = std::vector>; - /// @brief copy everything of @a src into @a dst but its type and value - static void copy_metadata(const basic_json& src, basic_json& dst) - { - // a custom base class is only required to be copy-constructible and - // move-assignable, so the copy has to go through a temporary - static_cast(dst) = json_base_class_t(static_cast(src)); + /// @brief tag selecting the constructor below; used only to build the + /// elements of a deep copy (@ref copy_array_level, @ref copy_object_level) + struct copy_construct_tag {}; + public: + /*! + @brief construct a null value whose base class - and, with @ref + JSON_DIAGNOSTIC_POSITIONS, positions - are copied from @a src + + Copy-constructing @ref json_base_class_t here, rather than default- + constructing the element and assigning its base class afterwards, means + that copying a @ref basic_json only ever requires a copy-constructible + base class, and never a move-assignable one as well. + + @note this constructor has to be public: @ref copy_array_level and + @ref copy_object_level reach it through @ref array_t's or @ref + object_t's own emplace_back(), which constructs the element from + outside @ref basic_json and so cannot call a private constructor. + @ref copy_construct_tag is private, though, and nothing in the + public interface hands out a value of it, so outside code can still + never name it to call this constructor itself. + */ + basic_json(copy_construct_tag /*unused*/, const basic_json& src) + : json_base_class_t(src) #if JSON_DIAGNOSTIC_POSITIONS - dst.start_position = src.start_position; - dst.end_position = src.end_position; + , start_position(src.start_position) + , end_position(src.end_position) #endif + { } + private: + /*! @brief copy the value of @a src into @a dst, which must not be structured @@ -27833,8 +27853,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } /*! - @brief copy everything of @a src into the null value @a dst but the children + @brief finish the copy @a dst of @a src that a @ref copy_construct_tag + constructor started, other than the children of an object or array + @a dst already has @a src's base class and, with @ref + JSON_DIAGNOSTIC_POSITIONS, positions; only its value is still missing. Objects and arrays are not copied here; they are appended to @a worklist to be created later by @ref copy_iteratively. Until that happens, @a dst remains a null value, so that a partially built copy can be destroyed at any point @@ -27842,8 +27865,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec */ static void copy_shallow(const basic_json& src, basic_json& dst, copy_worklist_t& worklist) { - copy_metadata(src, dst); - if (src.m_data.m_type == value_t::object || src.m_data.m_type == value_t::array) { // defer: dst stays a null value until its container exists @@ -27864,15 +27885,19 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { const array_t& src_array = *src.m_data.m_value.array; - // create all elements up front: growing the array afterwards could - // invalidate the pointers that are handed to the worklist; resize() - // rather than the fill constructor, because not every array type - // provides the latter (e.g., ones without a matching allocator-aware - // fill constructor) dst.m_data.m_value.array = create(); // only now that the array exists may dst stop being a null value dst.m_data.m_type = value_t::array; - dst.m_data.m_value.array->resize(src_array.size()); + + // create every element - its base class already copy-constructed from + // its counterpart in src, via the copy_construct_tag constructor - + // before any of their addresses are handed to worklist below: growing + // the array while that is going on could reallocate it and invalidate + // addresses taken from an earlier iteration + for (const auto& src_element : src_array) + { + dst.m_data.m_value.array->emplace_back(copy_construct_tag{}, src_element); + } auto dst_it = dst.m_data.m_value.array->begin(); for (auto src_it = src_array.cbegin(); src_it != src_array.cend(); ++src_it, ++dst_it) @@ -27890,12 +27915,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // build the complete key skeleton and hand it to the object's range // constructor: adding the keys one by one would be quadratic for object - // types that are backed by a vector, such as nlohmann::ordered_map + // types that are backed by a vector, such as nlohmann::ordered_map; each + // value's base class is already copy-constructed from its counterpart + // in src, via the copy_construct_tag constructor scratch.clear(); scratch.reserve(src_object.size()); for (const auto& element : src_object) { - scratch.emplace_back(element.first, basic_json()); + scratch.emplace_back(element.first, basic_json(copy_construct_tag{}, element.second)); } dst.m_data.m_value.object = create(std::make_move_iterator(scratch.begin()), diff --git a/tests/src/unit-custom-base-class.cpp b/tests/src/unit-custom-base-class.cpp index 138940d3d..a6b9b9ea4 100644 --- a/tests/src/unit-custom-base-class.cpp +++ b/tests/src/unit-custom-base-class.cpp @@ -405,3 +405,73 @@ TEST_CASE("JSON Visit Node") ); CHECK(expected.empty()); } + +// A custom base class with a const member: copy-constructible (initializing a +// const member works fine), but not copy-/move-assignable (assigning one does +// not). Used to check that copy construction never requires more than that. +struct const_member_base +{ + const int id = 7; // NOLINT(misc-non-private-member-variables-in-classes) +}; + +using json_with_const_base = nlohmann::basic_json < + std::map, + std::vector, + std::string, + bool, + std::int64_t, + std::uint64_t, + double, + std::allocator, + nlohmann::adl_serializer, + std::vector, + const_member_base + >; + +// build an array nested @a depth levels deep, with the innermost value 1; +// every level is constructed (never assigned), since const_member_base does +// not support assignment +static json_with_const_base make_nested_array(std::size_t depth) +{ + if (depth == 0) + { + return json_with_const_base(1); + } + return json_with_const_base::array({make_nested_array(depth - 1)}); +} + +TEST_CASE("Regression test for issue #5674 - copy construction must not require an assignable base class") +{ + SECTION("depth 0") + { + // as in the original bug report: copy construction only, no assignment + const json_with_const_base j = {1, 2}; + const json_with_const_base copy = j; // NOLINT(performance-unnecessary-copy-initialization) + + CHECK(copy.size() == 2); + CHECK(copy.id == 7); + } + + SECTION("nested deeper than the copy constructor's descent bound") + { + // beyond nesting_depth_limit() (128) levels, the copy constructor + // copies without the call stack (copy_iteratively / copy_array_level), + // which used to assign the base class of every element it created + const std::size_t depth = 300; + + const json_with_const_base j = make_nested_array(depth); + const json_with_const_base copy = j; // NOLINT(performance-unnecessary-copy-initialization) + + const json_with_const_base* c = © + for (std::size_t level = 0; level <= depth; ++level) + { + CAPTURE(level) + REQUIRE(c->id == 7); + if (level < depth) + { + c = &c->at(0); + } + } + CHECK(*c == 1); + } +} From 756b28c2b85d668c9e74e3fc3c4688039ebc2901 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:39 +0200 Subject: [PATCH 09/12] Keep a NUL byte ending a // comment as the end of input (#5696) With the default NUL handling (JSON_STRICT_NUL_HANDLING not set), a NUL byte in the input is treated as the real end of input everywhere - except when it immediately ends a `//` comment: scan_comment() matched '\0' as a comment terminator like '\n', so the NUL was consumed as part of the comment and scan() never saw it as end of input; the next get() then kept reading past it. Multi-line comments and JSON_STRICT_NUL_HANDLING=1 were unaffected, since there the NUL is just part of the comment text. Fix scan_comment() to leave the NUL unconsumed (unget()) instead of returning it as part of the comment, so the following scan() reports it as end of input, exactly as for a NUL anywhere else. Fixes #5659. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/input/lexer.hpp | 7 ++++- single_include/nlohmann/json.hpp | 7 ++++- tests/src/unit-class_parser.cpp | 39 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/input/lexer.hpp b/include/nlohmann/detail/input/lexer.hpp index a67a0228c..052bc4de9 100644 --- a/include/nlohmann/detail/input/lexer.hpp +++ b/include/nlohmann/detail/input/lexer.hpp @@ -941,10 +941,15 @@ class lexer : public lexer_base case '\n': case '\r': case char_traits::eof(): + return true; + #if !JSON_STRICT_NUL_HANDLING case '\0': -#endif + // a NUL byte is the end of the input (see scan()), + // so leave it for scan() to see + unget(); return true; +#endif default: break; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 349977636..884430274 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -11027,10 +11027,15 @@ class lexer : public lexer_base case '\n': case '\r': case char_traits::eof(): + return true; + #if !JSON_STRICT_NUL_HANDLING case '\0': -#endif + // a NUL byte is the end of the input (see scan()), + // so leave it for scan() to see + unget(); return true; +#endif default: break; diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index f67ba631b..75f3757e8 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -592,6 +592,45 @@ TEST_CASE("parser class") // parsing from a string literal is unaffected either way CHECK(json::parse("123") == json(123)); + + // a NUL byte that ends a // comment ends the input just + // like a NUL byte anywhere else (issue #5659); before the + // fix, the NUL was consumed as part of the comment, and + // scanning continued with whatever followed it + { + // same as "//c" alone (real end of input after the + // comment), rather than continuing with "[1]" + std::string s1 = "//c"; + s1.push_back('\0'); + s1 += "[1]"; + json _; // NOLINT(readability-identifier-naming) + CHECK_THROWS_WITH_AS(_ = json::parse(s1, nullptr, true, true), + "[json.exception.parse_error.101] parse error at line 1, column 4: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal", + json::parse_error&); + CHECK_FALSE(json::accept(s1, true, true)); + } + + { + // same as "[1, //c" alone, rather than continuing with " 2]" + std::string s2 = "[1, //c"; + s2.push_back('\0'); + s2 += " 2]"; + json _; // NOLINT(readability-identifier-naming) + CHECK_THROWS_WITH_AS(_ = json::parse(s2, nullptr, true, true), + "[json.exception.parse_error.101] parse error at line 1, column 8: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal", + json::parse_error&); + CHECK_FALSE(json::accept(s2, true, true)); + } + + { + // same as "1 //c" alone: the comment (and the NUL that + // ends it) is ignored, and "x" is never reached + std::string s3 = "1 //c"; + s3.push_back('\0'); + s3 += "x"; + CHECK(json::parse(s3, nullptr, true, true) == json(1)); + CHECK(json::accept(s3, true, true)); + } } #endif From 1edf0ef041fe20710cea8d236a4bab05d53b0fce Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:43 +0200 Subject: [PATCH 10/12] Fix value(json_pointer, default) aborting under JSON_NOEXCEPTION (#5700) With exceptions disabled (JSON_NOEXCEPTION or -fno-exceptions), value(const json_pointer&, default) called std::abort() for array reference tokens that array_index() rejects with out_of_range.404/410: indices too large to fit size_type, the empty token ("/"), and tokens like "/1a". With exceptions enabled, the same tokens correctly yielded the default value, because get_checked_or_null() relied on JSON_TRY/JSON_INTERNAL_CATCH (detail::out_of_range&) to turn the exception into nullptr; under JSON_NOEXCEPTION, JSON_THROW aborts before that catch is ever reached. get_checked_or_null() now detects those out-of-range tokens itself, the same way contains(json_pointer) already does (#5495), and only calls array_index() for tokens that must still raise parse_error.106 or parse_error.109 (e.g. "/01", "/+1"), matching the documented behavior of value(). Added regression tests to tests/src/unit-disabled_exceptions.cpp (built with JSON_NOEXCEPTION and -fno-exceptions) and the matching checks to tests/src/unit-element_access2.cpp for normal exception mode. Fixes #5672. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/json_pointer.hpp | 26 ++++++++++++++++-------- single_include/nlohmann/json.hpp | 26 ++++++++++++++++-------- tests/src/unit-disabled_exceptions.cpp | 15 ++++++++++++++ tests/src/unit-element_access2.cpp | 15 ++++++++++++++ 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index e34acb6d3..a10f4f39f 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -685,19 +685,29 @@ class json_pointer return nullptr; } - // may throw parse_error.106/109 for a malformed index; an - // index that is syntactically valid but cannot be - // represented (out_of_range.404/410) is treated like an - // out-of-range index below - typename BasicJsonType::size_type idx{}; - JSON_TRY + // tokens that array_index() rejects with parse_error.106/109 + // are passed on to it; all other tokens that it would reject + // with out_of_range.404/410 are detected here, so that this + // also works without exceptions + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 && !(reference_token[0] >= '1' && reference_token[0] <= '9'))) { - idx = array_index(reference_token); + static_cast(array_index(reference_token)); // throws parse_error.106/109 } - JSON_INTERNAL_CATCH (detail::out_of_range&) + if (JSON_HEDLEY_UNLIKELY(reference_token.empty() || !std::all_of(reference_token.begin(), reference_token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }))) { return nullptr; } + errno = 0; // strtoull() does not reset errno on success + char* p_end = nullptr; // NOLINT(misc-const-correctness) + const unsigned long long magnitude = std::strtoull(reference_token.data(), &p_end, 10); // NOLINT(runtime/int) + if (JSON_HEDLEY_UNLIKELY(errno == ERANGE || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) + { + return nullptr; + } + const auto idx = static_cast(magnitude); if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 884430274..1ec6e9cc5 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20281,19 +20281,29 @@ class json_pointer return nullptr; } - // may throw parse_error.106/109 for a malformed index; an - // index that is syntactically valid but cannot be - // represented (out_of_range.404/410) is treated like an - // out-of-range index below - typename BasicJsonType::size_type idx{}; - JSON_TRY + // tokens that array_index() rejects with parse_error.106/109 + // are passed on to it; all other tokens that it would reject + // with out_of_range.404/410 are detected here, so that this + // also works without exceptions + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 && !(reference_token[0] >= '1' && reference_token[0] <= '9'))) { - idx = array_index(reference_token); + static_cast(array_index(reference_token)); // throws parse_error.106/109 } - JSON_INTERNAL_CATCH (detail::out_of_range&) + if (JSON_HEDLEY_UNLIKELY(reference_token.empty() || !std::all_of(reference_token.begin(), reference_token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }))) { return nullptr; } + errno = 0; // strtoull() does not reset errno on success + char* p_end = nullptr; // NOLINT(misc-const-correctness) + const unsigned long long magnitude = std::strtoull(reference_token.data(), &p_end, 10); // NOLINT(runtime/int) + if (JSON_HEDLEY_UNLIKELY(errno == ERANGE || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) + { + return nullptr; + } + const auto idx = static_cast(magnitude); if (JSON_HEDLEY_UNLIKELY(idx >= ptr->m_data.m_value.array->size())) { diff --git a/tests/src/unit-disabled_exceptions.cpp b/tests/src/unit-disabled_exceptions.cpp index 0b8de64d3..8e3adf944 100644 --- a/tests/src/unit-disabled_exceptions.cpp +++ b/tests/src/unit-disabled_exceptions.cpp @@ -47,6 +47,21 @@ TEST_CASE("Tests with disabled exceptions") delete sax_no_exception::error_string; // NOLINT(cppcoreguidelines-owning-memory) } + SECTION("issue #5672 - value(json_pointer, default) must not abort for array tokens that are not a valid index") + { + const json j = {1, 2, 3}; + + // a syntactically valid index that is out of range for this array + CHECK(j.value("/7"_json_pointer, 42) == 42); + // a reference token that is not a number at all + CHECK(j.value("/1a"_json_pointer, 42) == 42); + // the empty reference token (JSON pointer "/") + CHECK(j.value("/"_json_pointer, 42) == 42); + // an index whose magnitude does not fit into size_type + CHECK(j.value("/99999999999999999999999"_json_pointer, 42) == 42); + CHECK(j.value("/18446744073709551615"_json_pointer, 42) == 42); + } + SECTION("growing an ordered_json object") { auto j = nlohmann::ordered_json::object(); diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index a64caff89..efd7b15a0 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -516,6 +516,21 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_array.value("/-"_json_pointer, 42) == 42); CHECK(j_array_const.value("/-"_json_pointer, 42) == 42); + // Test an index with a non-digit after a valid leading digit; this is + // out_of_range (not parse_error) and must not throw (see #5672) + CHECK(j_array.value("/1a"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/1a"_json_pointer, 42) == 42); + + // Test the empty reference token (JSON pointer "/"); see #5672 + CHECK(j_array.value("/"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/"_json_pointer, 42) == 42); + + // Test an index whose magnitude does not fit into size_type (see #5672) + CHECK(j_array.value("/99999999999999999999999"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/99999999999999999999999"_json_pointer, 42) == 42); + CHECK(j_array.value("/18446744073709551615"_json_pointer, 42) == 42); + CHECK(j_array_const.value("/18446744073709551615"_json_pointer, 42) == 42); + #if !defined(JSON_NOEXCEPTION) // Test malformed index (non-numeric) throws parse_error CHECK_THROWS_WITH_AS(j_array.value("/foo"_json_pointer, 1), "[json.exception.parse_error.109] parse error: array index 'foo' is not a number", typename Json::parse_error&); From a212d3b2e4818849a57270bb537d74564be77692 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:47 +0200 Subject: [PATCH 11/12] Preserve the object comparator's state in a deep copy past the nesting bound (#5722) * Preserve the object comparator's state in a deep copy past the nesting bound copy_object_level(), used by the copy constructor and copy assignment once a value is nested deeper than the iterative deep copy's bound (128 levels, or every copy under JSON_NO_THREAD_LOCAL), built each object's copy with the object type's plain range constructor. That default-constructs the object's comparator instead of copying the original's. For an object type whose comparator carries state, such as a std::map that compares keys case-sensitively only when constructed that way, the copy then ordered - and could even deduplicate - its keys differently from the original. Add detail::is_comparator_constructible_object_type, a detection trait for object types that provide a key_comp() and a constructor taking a range and a comparator, the way std::map does. copy_object_level now dispatches on it: an object type that qualifies gets its copy built with src_object.key_comp() passed along; other object types, such as nlohmann::ordered_map (which has a key_compare for its std::map-like interface, but no key_comp()), keep using the plain range constructor exactly as before. merge_patch and update() were checked for the same pattern; neither is affected, since both only ever add members one at a time to an object that already has its own comparator (or start a brand new default-constructed one), rather than rebuilding an object_t from a range copied out of an existing, possibly custom-comparator object. Fixes #5649. Signed-off-by: Niels Lohmann * Keep astyle from padding the create_object_with_comparator templates Spell the negated condition as detail::negation<...> instead of a leading '!', which made astyle spread the template header out. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- include/nlohmann/detail/meta/type_traits.hpp | 31 ++++++++ include/nlohmann/json.hpp | 24 +++++- single_include/nlohmann/json.hpp | 55 ++++++++++++- tests/src/unit-comparison.cpp | 81 ++++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 2f837e046..14029c0fc 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -189,6 +189,37 @@ struct actual_object_comparator template using actual_object_comparator_t = typename actual_object_comparator::type; +template +using detect_key_comp = decltype(std::declval().key_comp()); + +// whether ObjectType can be constructed from a pair of Iterator together with +// a copy of its own comparator, the way std::map can: it needs a nested +// key_compare, a const key_comp() convertible to it, and a matching +// (Iterator, Iterator, const key_compare&) constructor. +// +// used to preserve a stateful comparator when a copy is built from a range +// past the iterative deep copy's nesting bound (see copy_object_level); an +// object type that does not satisfy this, such as nlohmann::ordered_map +// (which has key_compare for its std::map-like interface, but no key_comp()), +// keeps default-constructing its comparator, just as it always has +template +struct is_comparator_constructible_object_type_impl : std::false_type {}; + +template +struct is_comparator_constructible_object_type_impl < + ObjectType, Iterator, enable_if_t::value >> +{ + using key_compare = typename ObjectType::key_compare; + + static constexpr bool value = + is_detected_convertible::value && + std::is_constructible::value; +}; + +template +struct is_comparator_constructible_object_type + : is_comparator_constructible_object_type_impl {}; + ///////////////// // char_traits // ///////////////// diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 268c28c42..c8c9854ca 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1176,6 +1176,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } } + /// @brief create the object type from a range, preserving @a src_object's + /// comparator when the object type supports it + /// Enabled for object types that provide a key_comp() and a matching + /// range-plus-comparator constructor, such as std::map. Other object + /// types, such as nlohmann::ordered_map, fall back to the plain range + /// constructor and default-construct their comparator, just as they + /// always have (@ref detail::is_comparator_constructible_object_type). + template::value, int> = 0> + static object_t* create_object_with_comparator(const object_t& src_object, Iterator first, Iterator last) + { + return create(first, last, src_object.key_comp()); + } + + template>::value, int> = 0> + static object_t* create_object_with_comparator(const object_t& /*src_object*/, Iterator first, Iterator last) + { + return create(first, last); + } + /// @brief create the copy of the object @a src in @a dst /// @note structured values are appended to @a worklist instead static void copy_object_level(const basic_json& src, basic_json& dst, @@ -1195,7 +1216,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec scratch.emplace_back(element.first, basic_json(copy_construct_tag{}, element.second)); } - dst.m_data.m_value.object = create(std::make_move_iterator(scratch.begin()), + dst.m_data.m_value.object = create_object_with_comparator(src_object, + std::make_move_iterator(scratch.begin()), std::make_move_iterator(scratch.end())); // only now that the object exists may dst stop being a null value dst.m_data.m_type = value_t::object; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 1ec6e9cc5..84ad40c74 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -4189,6 +4189,37 @@ struct actual_object_comparator template using actual_object_comparator_t = typename actual_object_comparator::type; +template +using detect_key_comp = decltype(std::declval().key_comp()); + +// whether ObjectType can be constructed from a pair of Iterator together with +// a copy of its own comparator, the way std::map can: it needs a nested +// key_compare, a const key_comp() convertible to it, and a matching +// (Iterator, Iterator, const key_compare&) constructor. +// +// used to preserve a stateful comparator when a copy is built from a range +// past the iterative deep copy's nesting bound (see copy_object_level); an +// object type that does not satisfy this, such as nlohmann::ordered_map +// (which has key_compare for its std::map-like interface, but no key_comp()), +// keeps default-constructing its comparator, just as it always has +template +struct is_comparator_constructible_object_type_impl : std::false_type {}; + +template +struct is_comparator_constructible_object_type_impl < + ObjectType, Iterator, enable_if_t::value >> +{ + using key_compare = typename ObjectType::key_compare; + + static constexpr bool value = + is_detected_convertible::value && + std::is_constructible::value; +}; + +template +struct is_comparator_constructible_object_type + : is_comparator_constructible_object_type_impl {}; + ///////////////// // char_traits // ///////////////// @@ -27921,6 +27952,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } } + /// @brief create the object type from a range, preserving @a src_object's + /// comparator when the object type supports it + /// Enabled for object types that provide a key_comp() and a matching + /// range-plus-comparator constructor, such as std::map. Other object + /// types, such as nlohmann::ordered_map, fall back to the plain range + /// constructor and default-construct their comparator, just as they + /// always have (@ref detail::is_comparator_constructible_object_type). + template::value, int> = 0> + static object_t* create_object_with_comparator(const object_t& src_object, Iterator first, Iterator last) + { + return create(first, last, src_object.key_comp()); + } + + template>::value, int> = 0> + static object_t* create_object_with_comparator(const object_t& /*src_object*/, Iterator first, Iterator last) + { + return create(first, last); + } + /// @brief create the copy of the object @a src in @a dst /// @note structured values are appended to @a worklist instead static void copy_object_level(const basic_json& src, basic_json& dst, @@ -27940,7 +27992,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec scratch.emplace_back(element.first, basic_json(copy_construct_tag{}, element.second)); } - dst.m_data.m_value.object = create(std::make_move_iterator(scratch.begin()), + dst.m_data.m_value.object = create_object_with_comparator(src_object, + std::make_move_iterator(scratch.begin()), std::make_move_iterator(scratch.end())); // only now that the object exists may dst stop being a null value dst.m_data.m_type = value_t::object; diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 4b66f1d07..15715fec2 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -849,6 +849,46 @@ Json nest(Json j, const std::size_t depth) return j; } +// a std::map comparator with state: case-insensitive, unless constructed +// case-sensitive. Used to check that copying an object copies the original's +// comparator rather than default-constructing a new one (see #5649). +struct key_case_less +{ + key_case_less() = default; + explicit key_case_less(const bool cs) noexcept : case_sensitive(cs) {} + + bool operator()(const std::string& a, const std::string& b) const + { + if (case_sensitive) + { + return a < b; + } + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), + [](unsigned char x, unsigned char y) + { + return std::tolower(x) < std::tolower(y); + }); + } + + bool case_sensitive = false; +}; + +template +using key_case_map = std::map; +using key_case_json = nlohmann::basic_json; + +// the innermost value of a chain of single-element arrays +template +const Json& innermost(const Json& j) +{ + const Json* p = &j; + while (p->is_array()) + { + p = &(*p)[0]; + } + return *p; +} + // orders keys case-insensitively, so "key" and "KEY" compare equivalent // (neither less than the other) although they are not equal struct case_insensitive_less @@ -913,6 +953,47 @@ TEST_CASE("equality of objects whose entries have no fixed order") } } +TEST_CASE("copying an object preserves its comparator's state") +{ + // Past the iterative deep copy's nesting bound, an object copy used to be + // built with a default-constructed comparator instead of a copy of the + // original's. For an object type whose comparator carries state - here, a + // std::map that compares keys case-sensitively only when created that way + // - this reordered the copy's keys and could even drop entries that the + // original's comparator kept distinct (see #5649). + key_case_json object = key_case_json::object_t(key_case_less(true)); // case-sensitive + object["b"] = 1; + object["B"] = 2; + object["a"] = 3; + REQUIRE(object.dump() == R"({"B":2,"a":3,"b":1})"); + + for (const std::size_t depth : std::vector {0, 127, 128, 200}) + { + CAPTURE(depth); + + key_case_json original = object; + for (std::size_t i = 0; i < depth; ++i) + { + original = key_case_json::array({std::move(original)}); + } + + { + const key_case_json copy = original; // NOLINT(performance-unnecessary-copy-initialization) + CHECK(innermost(copy).size() == 3); + CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})"); + CHECK(copy == original); + } + + { + key_case_json copy = key_case_json::array(); + copy = original; + CHECK(innermost(copy).size() == 3); + CHECK(innermost(copy).dump() == R"({"B":2,"a":3,"b":1})"); + CHECK(copy == original); + } + } +} + TEST_CASE("equality of an object whose comparator treats different keys as equivalent") { // https://github.com/nlohmann/json/issues/5655: past the nesting bound, From 1df1e8a845350f1156f8b4ac24887d3f40b4e71a Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sun, 4 Oct 2026 11:46:52 +0200 Subject: [PATCH 12/12] Make cross-string-type basic_json conversion explicit without implicit conversions (#5591) * Make cross-string-type basic_json conversion explicit without implicit conversions The converting constructor from another basic_json specialization was always implicit, so a value with a different string_t (std::wstring, a string with a custom allocator, ...) silently converted into a temporary, e.g. when passed to a function taking const nlohmann::json&. Such conversions do not produce correct values (#3425), and JSON_USE_IMPLICIT_CONVERSIONS=0 did not catch them. When JSON_USE_IMPLICIT_CONVERSIONS is 0, the constructor is now explicit if the string types differ. Specializations sharing a string type (json and ordered_json, different serializers or object maps) stay implicitly convertible, so the NLOHMANN_DEFINE_TYPE_* macros keep working with nested json members. get() constructs explicitly and works in both modes. Fixes #2649. Signed-off-by: Niels Lohmann * Construct explicitly in get_to() and to_json(std::optional) With JSON_USE_IMPLICIT_CONVERSIONS=0 the conversion from a basic_json with a different string type is now explicit, but two library paths still assigned such a value implicitly and failed to compile inside the library: - get_to() with a basic_json target (the #2175 overload) did `v = *this`, so json(42).get_to(alt_json&) broke although get() works. - to_json(BasicJsonType&, const std::optional&) is constrained on std::is_constructible (which accepts the explicit constructor) but did `j = *opt`, so converting a std::optional into a json broke. Both now construct the value explicitly, as get_impl() already does. Also replace static_cast(JSON_USE_IMPLICIT_CONVERSIONS) with a comparison: clang-tidy's modernize-use-bool-literals rejected the cast of the integer literal the macro expands to, failing ci_clang_tidy. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/basic_json.md | 12 +++++- .../macros/json_use_implicit_conversions.md | 24 ++++++++++- .../nlohmann/detail/conversions/to_json.hpp | 4 +- include/nlohmann/json.hpp | 37 +++++++++++++++-- single_include/nlohmann/json.hpp | 41 +++++++++++++++++-- tests/src/unit-alt-string.cpp | 36 ++++++++++++++++ 6 files changed, 144 insertions(+), 10 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/basic_json.md b/docs/mkdocs/docs/api/basic_json/basic_json.md index 1a0b101bd..78fa3560c 100644 --- a/docs/mkdocs/docs/api/basic_json/basic_json.md +++ b/docs/mkdocs/docs/api/basic_json/basic_json.md @@ -293,6 +293,15 @@ basic_json(basic_json&& other) noexcept; When used without parentheses around an empty initializer list, `basic_json()` is called instead of this function, yielding the JSON `#!json null` value. +- Overload 4: + + !!! info "Implicit conversion" + + The conversion is implicit unless [`JSON_USE_IMPLICIT_CONVERSIONS`](../macros/json_use_implicit_conversions.md) + is defined to `0` and `BasicJsonType::string_t` differs from `string_t`. In that case, the constructor is + `explicit`, so a JSON value with a different string type is no longer silently converted, for example when it is + passed to a function taking `#!cpp const json&`. Write `#!cpp json(other)` or `#!cpp other.get()` instead. + - Overload 7: !!! info "Preconditions" @@ -466,7 +475,8 @@ basic_json(basic_json&& other) noexcept; 1. Since version 1.0.0. 2. Since version 1.0.0. 3. Since version 2.1.0. -4. Since version 3.2.0. +4. Since version 3.2.0. Explicit for different string types if `JSON_USE_IMPLICIT_CONVERSIONS` is `0` since + version 3.13.0. 5. Since version 1.0.0. 6. Since version 1.0.0. 7. Since version 1.0.0. Fixed in version 3.13.0 to also check the iterator range for binary values; before, a range 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 5b74f1ff7..11bf74b22 100644 --- a/docs/mkdocs/docs/api/macros/json_use_implicit_conversions.md +++ b/docs/mkdocs/docs/api/macros/json_use_implicit_conversions.md @@ -5,7 +5,9 @@ ``` When defined to `0`, implicit conversions are switched off. By default, implicit conversions are switched on. The -value directly affects [`operator ValueType`](../basic_json/operator_ValueType.md). +value directly affects [`operator ValueType`](../basic_json/operator_ValueType.md) and the +[converting constructor](../basic_json/basic_json.md) from a `basic_json` specialization with a different string +type (overload 4). ## Default definition @@ -59,6 +61,25 @@ By default, implicit conversions are enabled. auto s = j.get(); ``` +??? example "Conversion between `basic_json` specializations" + + A `basic_json` specialization with a different string type is also no longer converted implicitly when + `JSON_USE_IMPLICIT_CONVERSIONS` is defined to `0`: + + ```cpp + using wjson = nlohmann::basic_json; + + void load(const nlohmann::json& j); + + wjson wj = /* ... */; + load(wj); // error: no implicit conversion + load(nlohmann::json(wj)); // OK: explicit conversion + load(wj.get()); // OK: explicit conversion + ``` + + Specializations that share the same string type, such as `json` and `ordered_json`, remain implicitly + convertible. + ## See also - [**operator ValueType**](../basic_json/operator_ValueType.md) - get a value (implicit) @@ -68,3 +89,4 @@ By default, implicit conversions are enabled. ## Version history - Added in version 3.9.0. +- Also affects the conversion between `basic_json` specializations with different string types since version 3.13.0. diff --git a/include/nlohmann/detail/conversions/to_json.hpp b/include/nlohmann/detail/conversions/to_json.hpp index 49b3c32e2..7b97068f3 100644 --- a/include/nlohmann/detail/conversions/to_json.hpp +++ b/include/nlohmann/detail/conversions/to_json.hpp @@ -294,7 +294,9 @@ void to_json(BasicJsonType& j, const std::optional& opt) noexcept(std::is_not { if (opt.has_value()) { - j = *opt; + // explicit construction, as the conversion from a basic_json with a different + // string type is explicit if JSON_USE_IMPLICIT_CONVERSIONS is 0 (#2649) + j = BasicJsonType(*opt); } else { diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index c8c9854ca..87e1dbc04 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1950,12 +1950,42 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec assert_invariant(); } + private: + /// whether a basic_json specialization can be converted implicitly into this one; + /// with JSON_USE_IMPLICIT_CONVERSIONS set to 0, this is only the case if both share + /// the same string type (see https://github.com/nlohmann/json/issues/2649) + template + using is_implicitly_convertible_basic_json = std::integral_constant < bool, + (JSON_USE_IMPLICIT_CONVERSIONS != 0) + || std::is_same::value >; + + /// tag to select the constructor that performs the conversion from another basic_json specialization + struct convert_basic_json_tag {}; + + public: /// @brief create a JSON value from an existing one /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ template < typename BasicJsonType, detail::enable_if_t < - detail::is_basic_json::value&& !std::is_same::value, int > = 0 > + detail::is_basic_json::value&& !std::is_same::value + && is_implicitly_convertible_basic_json::value, int > = 0 > basic_json(const BasicJsonType& val) + : basic_json(val, convert_basic_json_tag{}) + {} + + /// @brief create a JSON value from an existing one + /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ + template < typename BasicJsonType, + detail::enable_if_t < + detail::is_basic_json::value&& !std::is_same::value + && !is_implicitly_convertible_basic_json::value, int > = 0 > + explicit basic_json(const BasicJsonType& val) + : basic_json(val, convert_basic_json_tag{}) + {} + + private: + template + basic_json(const BasicJsonType& val, convert_basic_json_tag /*unused*/) #if JSON_DIAGNOSTIC_POSITIONS : start_position(val.start_pos()), end_position(val.end_pos()) @@ -1975,6 +2005,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec assert_invariant(); } + public: /// @brief create a container (array or object) from an initializer list /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(initializer_list_t init, @@ -2741,7 +2772,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec int > = 0 > BasicJsonType get_impl(detail::priority_tag<2> /*unused*/) const { - return *this; + return BasicJsonType(*this); } /*! @@ -2880,7 +2911,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec int> = 0> ValueType & get_to(ValueType& v) const { - v = *this; + v = ValueType(*this); return v; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 84ad40c74..a91bd9361 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -6960,7 +6960,9 @@ void to_json(BasicJsonType& j, const std::optional& opt) noexcept(std::is_not { if (opt.has_value()) { - j = *opt; + // explicit construction, as the conversion from a basic_json with a different + // string type is explicit if JSON_USE_IMPLICIT_CONVERSIONS is 0 (#2649) + j = BasicJsonType(*opt); } else { @@ -28726,12 +28728,42 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec assert_invariant(); } + private: + /// whether a basic_json specialization can be converted implicitly into this one; + /// with JSON_USE_IMPLICIT_CONVERSIONS set to 0, this is only the case if both share + /// the same string type (see https://github.com/nlohmann/json/issues/2649) + template + using is_implicitly_convertible_basic_json = std::integral_constant < bool, + (JSON_USE_IMPLICIT_CONVERSIONS != 0) + || std::is_same::value >; + + /// tag to select the constructor that performs the conversion from another basic_json specialization + struct convert_basic_json_tag {}; + + public: /// @brief create a JSON value from an existing one /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ template < typename BasicJsonType, detail::enable_if_t < - detail::is_basic_json::value&& !std::is_same::value, int > = 0 > + detail::is_basic_json::value&& !std::is_same::value + && is_implicitly_convertible_basic_json::value, int > = 0 > basic_json(const BasicJsonType& val) + : basic_json(val, convert_basic_json_tag{}) + {} + + /// @brief create a JSON value from an existing one + /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ + template < typename BasicJsonType, + detail::enable_if_t < + detail::is_basic_json::value&& !std::is_same::value + && !is_implicitly_convertible_basic_json::value, int > = 0 > + explicit basic_json(const BasicJsonType& val) + : basic_json(val, convert_basic_json_tag{}) + {} + + private: + template + basic_json(const BasicJsonType& val, convert_basic_json_tag /*unused*/) #if JSON_DIAGNOSTIC_POSITIONS : start_position(val.start_pos()), end_position(val.end_pos()) @@ -28751,6 +28783,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec assert_invariant(); } + public: /// @brief create a container (array or object) from an initializer list /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(initializer_list_t init, @@ -29517,7 +29550,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec int > = 0 > BasicJsonType get_impl(detail::priority_tag<2> /*unused*/) const { - return *this; + return BasicJsonType(*this); } /*! @@ -29656,7 +29689,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec int> = 0> ValueType & get_to(ValueType& v) const { - v = *this; + v = ValueType(*this); return v; } diff --git a/tests/src/unit-alt-string.cpp b/tests/src/unit-alt-string.cpp index 8b86c78e8..e503a4914 100644 --- a/tests/src/unit-alt-string.cpp +++ b/tests/src/unit-alt-string.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -423,6 +424,41 @@ TEST_CASE("alternative string type") CHECK(j2.dump() == R"({"/foo/0":"bar","/foo/1":"baz"})"); } + SECTION("conversion between basic_json specializations (#2649)") + { + // explicit conversions are always possible + CHECK(std::is_constructible::value); + CHECK(std::is_constructible::value); + CHECK(std::is_constructible::value); + CHECK(std::is_constructible::value); + + // specializations with the same string type are implicitly convertible + CHECK(std::is_convertible::value); + CHECK(std::is_convertible::value); + + // specializations with different string types are only implicitly convertible + // if implicit conversions are enabled +#if JSON_USE_IMPLICIT_CONVERSIONS + CHECK(std::is_convertible::value); + CHECK(std::is_convertible::value); +#else + CHECK_FALSE(std::is_convertible::value); + CHECK_FALSE(std::is_convertible::value); +#endif + + // get() works in either case + const nlohmann::json j = {{"foo", 1}, {"bar", true}}; + CHECK(j.get() == nlohmann::ordered_json(j)); + // (only a number is converted here, as objects and strings are affected by #3425) + CHECK(nlohmann::json(42).get() == 42); + CHECK(alt_json(nlohmann::json(42)) == 42); + + // get_to() also works in either case + alt_json a; + nlohmann::json(42).get_to(a); + CHECK(a == 42); + } + SECTION("strict enum") { // regression test for #5667: NLOHMANN_JSON_SERIALIZE_ENUM_STRICT's from_json