diff --git a/docs/mkdocs/docs/features/binary_formats/bjdata.md b/docs/mkdocs/docs/features/binary_formats/bjdata.md index faff64f16..d3f63a9b8 100644 --- a/docs/mkdocs/docs/features/binary_formats/bjdata.md +++ b/docs/mkdocs/docs/features/binary_formats/bjdata.md @@ -125,6 +125,7 @@ The library uses the following mapping from JSON values types to BJData types ac - `"_ArrayType_"` is one of `uint8`, `int8`, `uint16`, `int16`, `uint32`, `int32`, `uint64`, `int64`, `single`, `double`, `char`, or `byte`, + - `"_ArraySize_"` is an array, since the dimensions are written as the ND-array header's length, - every entry of `"_ArraySize_"` is a non-negative integer, and their product is representable as a `std::size_t`, - `"_ArrayData_"` holds exactly that many elements, and - every element of `"_ArrayData_"` is a number of the kind named by `"_ArrayType_"` (a floating-point number for diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index e636c6d84..b8e9efa45 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -1668,6 +1668,15 @@ class binary_writer CharType dtype = it->second; key = "_ArraySize_"; + // the dimensions are written verbatim as the header length below, so a + // value that is not an array cannot produce a valid one: null emits 'Z' + // and an object emits '{', neither of which a reader accepts after '#'. + // Such an object is not a valid ndarray and falls back to a plain object. + if (!value.at(key).is_array()) + { + return true; + } + std::size_t len = (value.at(key).empty() ? 0 : 1); for (const auto& el : value.at(key)) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 9c6ed1335..31bfb869d 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -18676,6 +18676,15 @@ class binary_writer CharType dtype = it->second; key = "_ArraySize_"; + // the dimensions are written verbatim as the header length below, so a + // value that is not an array cannot produce a valid one: null emits 'Z' + // and an object emits '{', neither of which a reader accepts after '#'. + // Such an object is not a valid ndarray and falls back to a plain object. + if (!value.at(key).is_array()) + { + return true; + } + std::size_t len = (value.at(key).empty() ? 0 : 1); for (const auto& el : value.at(key)) { diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index 41feb5288..7d0dd5ff2 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -2751,6 +2751,31 @@ TEST_CASE("BJData") CHECK(json::to_bjdata(j_ok) == std::vector({'[', '$', 'U', '#', '[', 'i', 2, 'i', 3, ']', 1, 2, 3, 4, 5, 6})); CHECK(json::from_bjdata(json::to_bjdata(j_ok), true, true) == j_ok); } + + SECTION("ndarray whose _ArraySize_ is not an array stays as object") + { + // the shape is written verbatim as the header length, so a + // value that is not an array cannot produce a valid one: null + // would emit 'Z' and an object '{', neither of which a reader + // accepts after '#'. Both have to stay plain objects. + json const j_null = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", nullptr}, {"_ArrayData_", json::array()}}); + const auto out_null = json::to_bjdata(j_null); + CHECK(out_null.at(0) == '{'); + CHECK(json::from_bjdata(out_null) == j_null); + + // an object shape passes the per-entry check by iterating its + // values rather than dimensions, so it needs rejecting too + json const j_obj = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {{"a", 1}}}, {"_ArrayData_", {1}}}); + const auto out_obj = json::to_bjdata(j_obj); + CHECK(out_obj.at(0) == '{'); + CHECK(json::from_bjdata(out_obj) == j_obj); + + // a scalar shape is not a dimension list either + json const j_num = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", 1}, {"_ArrayData_", {1}}}); + const auto out_num = json::to_bjdata(j_num); + CHECK(out_num.at(0) == '{'); + CHECK(json::from_bjdata(out_num) == j_num); + } } }