diff --git a/docs/mkdocs/docs/api/basic_json/get.md b/docs/mkdocs/docs/api/basic_json/get.md index 016595d5f..06e34218a 100644 --- a/docs/mkdocs/docs/api/basic_json/get.md +++ b/docs/mkdocs/docs/api/basic_json/get.md @@ -120,6 +120,16 @@ overload (3). See [Number conversion](../../features/types/number_handling.md#number-conversion) for more information. +!!! note "Booleans as numbers" + + A JSON boolean converts to `#!cpp 0` or `#!cpp 1` for every arithmetic type **except** [`number_integer_t`](number_integer_t.md), + [`number_unsigned_t`](number_unsigned_t.md) and [`number_float_t`](number_float_t.md) (by default `#!cpp std::int64_t`, + `#!cpp std::uint64_t`, and `#!cpp double`), for which `#!cpp get()` throws + [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) ("type must be number, but is boolean"). So + `#!cpp json(true).get()` is `#!cpp 1`, while `#!cpp json(true).get()` and + `#!cpp json(true).get()` throw. This is the behavior of all 3.x versions; + [`basic_json_view::get()`](../basic_json_view/get.md) behaves the same. + !!! note "`std::optional` conversions" Prior to version 3.13.0, `#!cpp get>()` (and other conversions to `std::optional`) failed to diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index 7ee7952bf..1739d372b 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get.md +++ b/docs/mkdocs/docs/api/basic_json_view/get.md @@ -11,7 +11,8 @@ For the types below, the conversion works directly on the flat index — no `Bas - `#!cpp bool` - arithmetic types other than `#!cpp bool` (for a boolean, `#!cpp 0`/`#!cpp 1`, exactly as for - [`BasicJsonType::get()`](../basic_json/get.md)) + [`BasicJsonType::get()`](../basic_json/get.md#notes), except for `number_integer_t`, `number_unsigned_t` and + `number_float_t`, which throw `type_error.302` for a boolean, as `BasicJsonType::get()` does) - `#!cpp std::nullptr_t` - `#!cpp std::basic_string` (including `string_t`) — a copy of the string - [`string_view_t`](index.md#member-types) — **no copy**: the returned view points into the document's diff --git a/include/nlohmann/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp index ffdcc3605..81b175e2b 100644 --- a/include/nlohmann/detail/view/value.hpp +++ b/include/nlohmann/detail/view/value.hpp @@ -33,12 +33,37 @@ namespace view template struct value_tag {}; +/// whether basic_json reads T with a dedicated from_json() overload that does +/// not accept a boolean: exactly number_integer_t, number_unsigned_t and +/// number_float_t (the other arithmetic types convert a boolean to 0 or 1) +template +struct rejects_boolean : std::integral_constant < bool, + std::is_same::value + || std::is_same::value + || std::is_same::value > +{}; + +/// a boolean as T: 0 or 1 +template +NLOHMANN_VIEW_ALWAYS_INLINE T boolean_as(const document_data& /*d*/, const node& n, std::false_type /*rejects*/) +{ + return static_cast((n.flags & node_flags::is_true) != 0); +} + +/// a boolean as number_integer_t, number_unsigned_t or number_float_t: type_error.302, as in basic_json +template +[[noreturn]] NLOHMANN_VIEW_ALWAYS_INLINE T boolean_as(const document_data& d, const node& n, std::true_type /*rejects*/) +{ + throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast(n.kind))); +} + /*! @brief the number or boolean of a node converted to an arithmetic type As basic_json's get() for arithmetic types: integers and floats are -converted with static_cast, booleans give 0 or 1, and other types throw -type_error.302. +converted with static_cast, booleans give 0 or 1 (but throw type_error.302 for +number_integer_t, number_unsigned_t and number_float_t, as basic_json does), +and other types throw type_error.302. */ template NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const node& n) @@ -52,7 +77,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod case value_t::number_float: return static_cast(float_value(d, n)); case value_t::boolean: - return static_cast((n.flags & node_flags::is_true) != 0); + return boolean_as(d, n, rejects_boolean {}); case value_t::null: case value_t::object: case value_t::array: diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 6bb6ec771..4a7b949a8 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -10072,12 +10072,37 @@ namespace view template struct value_tag {}; +/// whether basic_json reads T with a dedicated from_json() overload that does +/// not accept a boolean: exactly number_integer_t, number_unsigned_t and +/// number_float_t (the other arithmetic types convert a boolean to 0 or 1) +template +struct rejects_boolean : std::integral_constant < bool, + std::is_same::value + || std::is_same::value + || std::is_same::value > +{}; + +/// a boolean as T: 0 or 1 +template +NLOHMANN_VIEW_ALWAYS_INLINE T boolean_as(const document_data& /*d*/, const node& n, std::false_type /*rejects*/) +{ + return static_cast((n.flags & node_flags::is_true) != 0); +} + +/// a boolean as number_integer_t, number_unsigned_t or number_float_t: type_error.302, as in basic_json +template +[[noreturn]] NLOHMANN_VIEW_ALWAYS_INLINE T boolean_as(const document_data& d, const node& n, std::true_type /*rejects*/) +{ + throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast(n.kind))); +} + /*! @brief the number or boolean of a node converted to an arithmetic type As basic_json's get() for arithmetic types: integers and floats are -converted with static_cast, booleans give 0 or 1, and other types throw -type_error.302. +converted with static_cast, booleans give 0 or 1 (but throw type_error.302 for +number_integer_t, number_unsigned_t and number_float_t, as basic_json does), +and other types throw type_error.302. */ template NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const node& n) @@ -10091,7 +10116,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod case value_t::number_float: return static_cast(float_value(d, n)); case value_t::boolean: - return static_cast((n.flags & node_flags::is_true) != 0); + return boolean_as(d, n, rejects_boolean {}); case value_t::null: case value_t::object: case value_t::array: diff --git a/tests/src/unit-conversions1.cpp b/tests/src/unit-conversions1.cpp index 3d39e10c9..888979440 100644 --- a/tests/src/unit-conversions1.cpp +++ b/tests/src/unit-conversions1.cpp @@ -1260,6 +1260,44 @@ TEST_CASE("value conversion") #undef JSON_HAS_CPP_17 #endif +TEST_CASE("get() of a boolean (#5988)") +{ + // a boolean converts to 0/1 for every arithmetic type except number_integer_t, number_unsigned_t and + // number_float_t, whose dedicated from_json() overloads throw + const json t = true; + const json f = false; + + SECTION("0 and 1") + { + // long and unsigned long are left out: they are std::int64_t and std::uint64_t on some platforms + CHECK(t.get() == true); + CHECK(f.get() == false); + CHECK(t.get() == 1); + CHECK(t.get() == 1); + CHECK(t.get() == 1); + CHECK(t.get() == 1); // NOLINT(google-runtime-int) + CHECK(t.get() == 1); // NOLINT(google-runtime-int) + CHECK(t.get() == 1); + CHECK(t.get() == 1); + CHECK(t.get() == 1.0F); + CHECK(t.get() == 1.0L); + CHECK(f.get() == 0); + CHECK(f.get() == 0.0F); + } + +#if !defined(JSON_NOEXCEPTION) + SECTION("number_integer_t, number_unsigned_t and number_float_t throw") + { + CHECK_THROWS_WITH_AS(t.get(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&); + CHECK_THROWS_WITH_AS(t.get(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&); + CHECK_THROWS_WITH_AS(t.get(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&); + CHECK_THROWS_WITH_AS(f.get(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&); + CHECK_THROWS_WITH_AS(f.get(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&); + CHECK_THROWS_WITH_AS(f.get(), "[json.exception.type_error.302] type must be number, but is boolean", json::type_error&); + } +#endif +} + #ifdef JSON_HAS_CPP_14 #undef JSON_HAS_CPP_14 #endif diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index c823ef9e2..8dc60e912 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -1551,6 +1551,85 @@ TEST_CASE("json_view values") } } +namespace +{ +// get() of a boolean: the same value or the same exception in basic_json and in the view (#5988) +template +void check_boolean_get_parity() +{ + for (const bool b : + { + true, false + }) + { + const std::string text = b ? "true" : "false"; + const json j = json::parse(text); + const json_document d = json_document::parse(text); + CAPTURE(text) +#if !defined(JSON_NOEXCEPTION) + T dom_value {}; + T view_value{}; + const std::string dom_error = json_view_test::exception_of_call([&] { dom_value = j.get(); }); + const std::string view_error = json_view_test::exception_of_call([&] { view_value = d.root().template get(); }); + CHECK(dom_error == view_error); + if (dom_error.empty()) + { + CHECK(dom_value == view_value); + CHECK(view_value == static_cast(b)); + } +#else + // without exceptions, the combinations that throw (number_integer_t, number_unsigned_t, number_float_t) + // cannot be run: JSON_THROW aborts + const bool throws = std::is_same::value + || std::is_same::value + || std::is_same::value; + if (!throws) + { + CHECK(j.get() == d.root().template get()); + } +#endif + } +} +} // namespace + +TEST_CASE("json_view get() of a boolean (#5988)") +{ + check_boolean_get_parity(); + check_boolean_get_parity(); + check_boolean_get_parity(); + check_boolean_get_parity(); + check_boolean_get_parity(); // NOLINT(google-runtime-int) + check_boolean_get_parity(); // NOLINT(google-runtime-int) + check_boolean_get_parity(); + check_boolean_get_parity(); + check_boolean_get_parity(); // NOLINT(google-runtime-int) + check_boolean_get_parity(); // NOLINT(google-runtime-int) + check_boolean_get_parity(); // NOLINT(google-runtime-int) + check_boolean_get_parity(); // NOLINT(google-runtime-int) + check_boolean_get_parity(); + check_boolean_get_parity(); + check_boolean_get_parity(); + check_boolean_get_parity(); + check_boolean_get_parity(); + +#if !defined(JSON_NOEXCEPTION) + SECTION("the exceptions of the exact number types") + { + const json_document d = json_document::parse("true"); + CHECK_THROWS_WITH_AS(d.root().get(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(d.root().get(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(d.root().get(), json_view_test::message("type_error.302", "", 0, 4, "type must be number, but is boolean").c_str(), json::type_error&); + CHECK(d.root().get() == 1); + // get_to() and value() go through get() + json::number_float_t x = 0; + CHECK_THROWS_AS(d.root().get_to(x), json::type_error&); + const json_document o = json_document::parse("{\"b\":true}"); + CHECK_THROWS_AS(static_cast(o.root().value("b", 0.0)), json::type_error&); + CHECK(o.root().value("b", 0) == 1); + } +#endif +} + TEST_CASE("json_view JSON pointers") { SECTION("every value of generated documents")