mirror of
https://github.com/nlohmann/json.git
synced 2026-09-24 14:20:11 +01:00
Test JSON_BRACE_INIT_COPY_SEMANTICS for real, and fix one-element tuples under it
The opt-in JSON_BRACE_INIT_COPY_SEMANTICS was never exercised by CI:
- Its only test, in unit-regression3.cpp, was guarded by
`#if defined(JSON_BRACE_INIT_COPY_SEMANTICS)` after the #include. The
header #undefs the macro unconditionally in macro_unscope.hpp, so the
guard was always false and the test compiled to nothing, whatever -D
flag was passed.
- The ci_test_brace_init_copy_semantics target that passes the flag was
not named by any workflow.
Move the test into its own translation unit that defines the macro before
including the header, as unit-diagnostics.cpp does for JSON_DIAGNOSTICS.
It now runs in every CI job and for every standard. Remove the unused
target: it ran the whole suite with the macro, and that suite deliberately
relies on default brace-init semantics in about 90 places
(e.g. `json({1})` meaning `[1]`), so it could never pass.
Running the whole suite with the macro did find one library bug:
to_json for std::tuple builds `j = { std::get<Idx>(t)... }`, so with copy
semantics a one-element tuple became its element. `json(std::tuple<int>{5})`
was `5` instead of `[5]`, and `get<std::tuple<int>>()` threw type_error.302
on the result. Under the macro, a one-element tuple now builds exactly what
the default deduction builds. Without the macro nothing changes.
The new tests also pin that the library's other conversions produce the
same values with and without the macro. The macro page now says that the
macro affects every single-element list (`json j = {1}` is `1`), and that
all translation units must agree on it, since it has no ABI tag.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
@@ -230,21 +230,6 @@ add_custom_target(ci_test_simdutf
|
||||
COMMENT "Compile and test with simdutf UTF-8 validation enabled"
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# Enable brace-init copy semantics.
|
||||
###############################################################################
|
||||
|
||||
add_custom_target(ci_test_brace_init_copy_semantics
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DCMAKE_BUILD_TYPE=Debug -GNinja
|
||||
-DJSON_BuildTests=ON -DJSON_FastTests=ON
|
||||
-DCMAKE_CXX_FLAGS=-DJSON_BRACE_INIT_COPY_SEMANTICS=1
|
||||
-S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics
|
||||
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics
|
||||
COMMAND cd ${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure
|
||||
COMMENT "Compile and test with brace-init copy semantics enabled"
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# Enable strict NUL-byte handling.
|
||||
###############################################################################
|
||||
|
||||
@@ -38,6 +38,27 @@ The default value is `0` (disabled — existing behavior is preserved).
|
||||
|
||||
This macro must be defined **before** including `<nlohmann/json.hpp>`. Defining it after the include has no effect.
|
||||
|
||||
!!! warning "Applies to every single-element list"
|
||||
|
||||
The macro does not only affect a single JSON value in braces. **Any** single-element braced list is treated as its
|
||||
element, so it no longer creates a one-element array:
|
||||
|
||||
```cpp
|
||||
json j1 = {1}; // 1, not [1]
|
||||
json j2 = {"text"}; // "text", not ["text"]
|
||||
json j3 = {{1, 2}}; // [1,2], not [[1,2]]
|
||||
```
|
||||
|
||||
Code that relies on these producing arrays must use `json::array()` instead (see below). Lists with more than one
|
||||
element, and a single `[string, value]` pair such as `{{"key", "value"}}`, which still creates an object, are not
|
||||
affected. The library's own conversions are not affected either: for example, `std::tuple<int>{5}` still becomes
|
||||
`[5]`.
|
||||
|
||||
!!! warning "Define it consistently"
|
||||
|
||||
The macro changes the behavior of a `basic_json` constructor but not the library's ABI tag, so all translation units
|
||||
of a program must agree on its value. Mixing translation units compiled with and without it is an ODR violation.
|
||||
|
||||
!!! tip "Workaround without the macro"
|
||||
|
||||
To explicitly create a single-element array without enabling this macro, use `json::array()`:
|
||||
|
||||
@@ -471,6 +471,23 @@ inline void to_json_tuple_impl(BasicJsonType& j, const Tuple& t, index_sequence<
|
||||
j = { std::get<Idx>(t)... };
|
||||
}
|
||||
|
||||
#if JSON_BRACE_INIT_COPY_SEMANTICS
|
||||
// JSON_BRACE_INIT_COPY_SEMANTICS makes a one-element braced list copy its
|
||||
// element instead of wrapping it, which would serialize std::tuple<int>{5} as 5
|
||||
// rather than [5]. Build what the default deduction builds instead: an object
|
||||
// if the element is a [string, value] pair, a one-element array otherwise.
|
||||
template<typename BasicJsonType, typename Tuple>
|
||||
inline void to_json_tuple_impl(BasicJsonType& j, const Tuple& t, index_sequence<0> /*unused*/)
|
||||
{
|
||||
BasicJsonType element(std::get<0>(t));
|
||||
// same test as the initializer-list constructor, including the cast that
|
||||
// keeps a string type constructible from 0 from selecting operator[](key)
|
||||
const bool is_member = element.is_array() && element.size() == 2
|
||||
&& element[static_cast<typename BasicJsonType::size_type>(0)].is_string();
|
||||
j = is_member ? BasicJsonType::object({std::move(element)}) : BasicJsonType::array({std::move(element)});
|
||||
}
|
||||
#endif
|
||||
|
||||
template<typename BasicJsonType, typename Tuple>
|
||||
inline void to_json_tuple_impl(BasicJsonType& j, const Tuple& /*unused*/, index_sequence<> /*unused*/)
|
||||
{
|
||||
|
||||
@@ -6758,6 +6758,23 @@ inline void to_json_tuple_impl(BasicJsonType& j, const Tuple& t, index_sequence<
|
||||
j = { std::get<Idx>(t)... };
|
||||
}
|
||||
|
||||
#if JSON_BRACE_INIT_COPY_SEMANTICS
|
||||
// JSON_BRACE_INIT_COPY_SEMANTICS makes a one-element braced list copy its
|
||||
// element instead of wrapping it, which would serialize std::tuple<int>{5} as 5
|
||||
// rather than [5]. Build what the default deduction builds instead: an object
|
||||
// if the element is a [string, value] pair, a one-element array otherwise.
|
||||
template<typename BasicJsonType, typename Tuple>
|
||||
inline void to_json_tuple_impl(BasicJsonType& j, const Tuple& t, index_sequence<0> /*unused*/)
|
||||
{
|
||||
BasicJsonType element(std::get<0>(t));
|
||||
// same test as the initializer-list constructor, including the cast that
|
||||
// keeps a string type constructible from 0 from selecting operator[](key)
|
||||
const bool is_member = element.is_array() && element.size() == 2
|
||||
&& element[static_cast<typename BasicJsonType::size_type>(0)].is_string();
|
||||
j = is_member ? BasicJsonType::object({std::move(element)}) : BasicJsonType::array({std::move(element)});
|
||||
}
|
||||
#endif
|
||||
|
||||
template<typename BasicJsonType, typename Tuple>
|
||||
inline void to_json_tuple_impl(BasicJsonType& j, const Tuple& /*unused*/, index_sequence<> /*unused*/)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
// This file tests the opt-in JSON_BRACE_INIT_COPY_SEMANTICS, so it defines the
|
||||
// macro itself rather than relying on a -D flag: the header #undefs the macro
|
||||
// before returning, so a test cannot check for it after the #include.
|
||||
#ifdef JSON_BRACE_INIT_COPY_SEMANTICS
|
||||
#undef JSON_BRACE_INIT_COPY_SEMANTICS
|
||||
#endif
|
||||
|
||||
#define JSON_BRACE_INIT_COPY_SEMANTICS 1
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
TEST_CASE("JSON_BRACE_INIT_COPY_SEMANTICS")
|
||||
{
|
||||
SECTION("single-element brace initialization copies the element (#5074)")
|
||||
{
|
||||
json const j_obj = {{"key", "value"}, {"num", 42}};
|
||||
json const j_arr = {1, 2, 3};
|
||||
|
||||
// object: brace init copies instead of wrapping
|
||||
json const j1{j_obj};
|
||||
CHECK(j1.is_object());
|
||||
CHECK(j1 == j_obj);
|
||||
|
||||
// array: brace init copies instead of wrapping
|
||||
json const j2{j_arr};
|
||||
CHECK(j2.is_array());
|
||||
CHECK(j2.size() == 3);
|
||||
CHECK(j2 == j_arr);
|
||||
|
||||
// this applies to any single element, not only to JSON values
|
||||
json const j3{true};
|
||||
CHECK(j3.is_boolean());
|
||||
|
||||
json const j4{42};
|
||||
CHECK(j4.is_number_integer());
|
||||
|
||||
json const j5 = {1};
|
||||
CHECK(j5 == 1);
|
||||
|
||||
json const j6 = {"text"};
|
||||
CHECK(j6 == "text");
|
||||
|
||||
json const j7 = {{1, 2}};
|
||||
CHECK(j7 == json::array({1, 2}));
|
||||
}
|
||||
|
||||
SECTION("what the macro does not change")
|
||||
{
|
||||
// lists with more than one element are unaffected
|
||||
json const j1 = {1, 2};
|
||||
CHECK(j1.is_array());
|
||||
CHECK(j1.size() == 2);
|
||||
|
||||
// a single [string, value] pair still describes an object
|
||||
json const j2 = {{"key", "value"}};
|
||||
CHECK(j2.is_object());
|
||||
CHECK(j2["key"] == "value");
|
||||
|
||||
// json::array() always creates an array
|
||||
json const j3 = json::array({1});
|
||||
CHECK(j3.is_array());
|
||||
CHECK(j3.size() == 1);
|
||||
CHECK(j3[0] == 1);
|
||||
|
||||
json const j_obj = {{"key", "value"}};
|
||||
json const j4 = json::array({j_obj});
|
||||
CHECK(j4.is_array());
|
||||
CHECK(j4.size() == 1);
|
||||
CHECK(j4[0] == j_obj);
|
||||
}
|
||||
|
||||
SECTION("conversions build the same values as without the macro")
|
||||
{
|
||||
SECTION("one-element std::tuple")
|
||||
{
|
||||
json const j1 = std::tuple<int> {5};
|
||||
CHECK(j1.dump() == "[5]");
|
||||
CHECK(std::get<0>(j1.get<std::tuple<int>>()) == 5);
|
||||
|
||||
json const j2 = std::tuple<std::string> {"text"};
|
||||
CHECK(j2.dump() == "[\"text\"]");
|
||||
CHECK(std::get<0>(j2.get<std::tuple<std::string>>()) == "text");
|
||||
|
||||
json const j3 = std::tuple<json> {json::array({1, 2})};
|
||||
CHECK(j3.dump() == "[[1,2]]");
|
||||
|
||||
// as without the macro, a [string, value] pair becomes an object
|
||||
// member (see the known limitation documented for std::pair)
|
||||
json const j4 = std::tuple<std::pair<std::string, int>> {{"a", 1}};
|
||||
CHECK(j4.dump() == "{\"a\":1}");
|
||||
}
|
||||
|
||||
SECTION("tuples with more elements")
|
||||
{
|
||||
json const j1 = std::tuple<int, std::string> {1, "a"};
|
||||
CHECK(j1.dump() == "[1,\"a\"]");
|
||||
|
||||
json const j2 = std::tuple<> {};
|
||||
CHECK(j2.dump() == "[]");
|
||||
}
|
||||
|
||||
SECTION("one-element containers")
|
||||
{
|
||||
json const j1 = std::vector<int> {1};
|
||||
CHECK(j1.dump() == "[1]");
|
||||
CHECK(j1.get<std::vector<int>>() == std::vector<int> {1});
|
||||
|
||||
std::array<int, 1> const arr = {{1}};
|
||||
json const j2 = arr;
|
||||
CHECK(j2.dump() == "[1]");
|
||||
|
||||
json const j3 = std::list<std::string> {"a"};
|
||||
CHECK(j3.dump() == "[\"a\"]");
|
||||
|
||||
json const j4 = std::map<std::string, int> {{"a", 1}};
|
||||
CHECK(j4.dump() == "{\"a\":1}");
|
||||
|
||||
json const j5 = std::map<int, int> {{1, 2}};
|
||||
CHECK(j5.dump() == "[[1,2]]");
|
||||
}
|
||||
|
||||
SECTION("std::pair")
|
||||
{
|
||||
json const j = std::pair<int, int> {1, 2};
|
||||
CHECK(j.dump() == "[1,2]");
|
||||
CHECK((j.get<std::pair<int, int>>() == std::pair<int, int> {1, 2}));
|
||||
}
|
||||
|
||||
SECTION("items()")
|
||||
{
|
||||
json j_obj = {{"key", 1}};
|
||||
for (auto& el : j_obj.items())
|
||||
{
|
||||
json const j = el;
|
||||
CHECK(j.dump() == "{\"key\":1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -658,33 +658,6 @@ TEST_CASE("regression test #5074 - portable workaround for single-element brace
|
||||
CHECK(j[0] == j_obj);
|
||||
}
|
||||
|
||||
#if defined(JSON_BRACE_INIT_COPY_SEMANTICS) && (JSON_BRACE_INIT_COPY_SEMANTICS == 1)
|
||||
TEST_CASE("regression test #5074 - single-element brace init with JSON_BRACE_INIT_COPY_SEMANTICS")
|
||||
{
|
||||
// with JSON_BRACE_INIT_COPY_SEMANTICS: single-element brace init copies/moves
|
||||
json const j_obj = {{"key", "value"}, {"num", 42}};
|
||||
json const j_arr = {1, 2, 3};
|
||||
|
||||
// object: brace init copies instead of wrapping
|
||||
json const j1{j_obj};
|
||||
CHECK(j1.is_object());
|
||||
CHECK(j1 == j_obj);
|
||||
|
||||
// array: brace init copies instead of wrapping
|
||||
json const j2{j_arr};
|
||||
CHECK(j2.is_array());
|
||||
CHECK(j2.size() == 3);
|
||||
CHECK(j2 == j_arr);
|
||||
|
||||
// primitives still work as initializer lists
|
||||
json const j3{true};
|
||||
CHECK(j3.is_boolean());
|
||||
|
||||
json const j4{42};
|
||||
CHECK(j4.is_number_integer());
|
||||
}
|
||||
#endif
|
||||
|
||||
struct Example_5122
|
||||
{
|
||||
float b = 2;
|
||||
|
||||
Reference in New Issue
Block a user