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<BasicJsonType>() constructs explicitly and
works in both modes.

Fixes #2649.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* 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<alt_json>() works.
- to_json(BasicJsonType&, const std::optional<T>&) is constrained on
  std::is_constructible (which accepts the explicit constructor) but did
  `j = *opt`, so converting a std::optional<alt_json> into a json broke.

Both now construct the value explicitly, as get_impl() already does.

Also replace static_cast<bool>(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 <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-10-04 11:46:52 +02:00
committed by GitHub
parent a212d3b2e4
commit 1df1e8a845
6 changed files with 144 additions and 10 deletions
+36
View File
@@ -13,6 +13,7 @@
#include <cstdint>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
@@ -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<nlohmann::json, alt_json>::value);
CHECK(std::is_constructible<alt_json, nlohmann::json>::value);
CHECK(std::is_constructible<nlohmann::json, nlohmann::ordered_json>::value);
CHECK(std::is_constructible<nlohmann::ordered_json, nlohmann::json>::value);
// specializations with the same string type are implicitly convertible
CHECK(std::is_convertible<nlohmann::ordered_json, nlohmann::json>::value);
CHECK(std::is_convertible<nlohmann::json, nlohmann::ordered_json>::value);
// specializations with different string types are only implicitly convertible
// if implicit conversions are enabled
#if JSON_USE_IMPLICIT_CONVERSIONS
CHECK(std::is_convertible<alt_json, nlohmann::json>::value);
CHECK(std::is_convertible<nlohmann::json, alt_json>::value);
#else
CHECK_FALSE(std::is_convertible<alt_json, nlohmann::json>::value);
CHECK_FALSE(std::is_convertible<nlohmann::json, alt_json>::value);
#endif
// get<BasicJsonType>() works in either case
const nlohmann::json j = {{"foo", 1}, {"bar", true}};
CHECK(j.get<nlohmann::ordered_json>() == nlohmann::ordered_json(j));
// (only a number is converted here, as objects and strings are affected by #3425)
CHECK(nlohmann::json(42).get<alt_json>() == 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