From b17c272f469c2ba0ca6439e3f77470862a19f739 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 9 Sep 2026 09:54:12 +0200 Subject: [PATCH] Reserve capacity in from_json() object conversion when the target container supports it (#5472) * Reserve capacity in from_json() object conversion when supported The object-to-container from_json() overload filled the target container one element at a time without reserving capacity, even when the target type supports reserve() (e.g. std::unordered_map) and the number of elements is already known. This caused unnecessary rehashing while parsing large objects into such containers. Add a reserve-detecting overload (from_json_object_impl), mirroring the priority_tag-based SFINAE technique already used by the array conversion path (from_json_array_impl), so that reserve(size()) is called up front when available and the loop falls back unchanged otherwise (e.g. for std::map). Fixes #5406 Signed-off-by: Niels Lohmann * Update doc example outputs for new object-conversion iteration order Reserving capacity in from_json()'s object-conversion path before inserting elements changes libstdc++'s std::unordered_map bucket layout, which changes the iteration order used by get__ValueType_const.cpp, get_to.cpp and operator__ValueType.cpp to print the elements of a converted std::unordered_map. Verified against a clean develop checkout (built with the same GCC/libstdc++ used in CI) that the old order was produced without this PR's change and the new order is produced with it, and that the three affected examples now match their updated expected output byte-for-byte. Signed-off-by: Niels Lohmann * Factor out a reserve-dispatch helper instead of duplicating the object from_json loop Addresses review feedback from @gregmarr on PR #5472: the emplace loop no longer needs to exist twice for the reserve/no-reserve cases. A small from_json_object_reserve() overload pair (SFINAE-dispatched on whether reserve() exists, mirroring the priority_tag technique used elsewhere) either calls reserve() or is a no-op; from_json_object_impl() calls it once. Signed-off-by: Niels Lohmann * Inline from_json_object_impl into from_json now that it is called only once Addresses review feedback from @gregmarr on PR #5472: with the reserve loop de-duplicated, from_json_object_impl no longer needs to be a separate function that from_json immediately delegates to. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../docs/examples/get__ValueType_const.output | 2 +- docs/mkdocs/docs/examples/get_to.output | 2 +- .../docs/examples/operator__ValueType.output | 2 +- .../nlohmann/detail/conversions/from_json.hpp | 12 +++++++ single_include/nlohmann/json.hpp | 12 +++++++ tests/src/unit-conversions.cpp | 31 +++++++++++++++++++ 6 files changed, 58 insertions(+), 3 deletions(-) diff --git a/docs/mkdocs/docs/examples/get__ValueType_const.output b/docs/mkdocs/docs/examples/get__ValueType_const.output index 5cd9cd3aa..e7e9b5d59 100644 --- a/docs/mkdocs/docs/examples/get__ValueType_const.output +++ b/docs/mkdocs/docs/examples/get__ValueType_const.output @@ -4,8 +4,8 @@ Hello, world! 1 2 3 4 5 -string: "Hello, world!" number: {"floating-point":17.23,"integer":42} null: null +string: "Hello, world!" boolean: true array: [1,2,3,4,5] diff --git a/docs/mkdocs/docs/examples/get_to.output b/docs/mkdocs/docs/examples/get_to.output index 5cd9cd3aa..e7e9b5d59 100644 --- a/docs/mkdocs/docs/examples/get_to.output +++ b/docs/mkdocs/docs/examples/get_to.output @@ -4,8 +4,8 @@ Hello, world! 1 2 3 4 5 -string: "Hello, world!" number: {"floating-point":17.23,"integer":42} null: null +string: "Hello, world!" boolean: true array: [1,2,3,4,5] diff --git a/docs/mkdocs/docs/examples/operator__ValueType.output b/docs/mkdocs/docs/examples/operator__ValueType.output index a3bd9fff4..de471ec02 100644 --- a/docs/mkdocs/docs/examples/operator__ValueType.output +++ b/docs/mkdocs/docs/examples/operator__ValueType.output @@ -4,9 +4,9 @@ Hello, world! 1 2 3 4 5 -string: "Hello, world!" number: {"floating-point":17.23,"integer":42} null: null +string: "Hello, world!" boolean: true array: [1,2,3,4,5] [json.exception.type_error.302] type must be boolean, but is string diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 6856a0965..11e40f5f4 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -398,6 +398,17 @@ inline void from_json(const BasicJsonType& j, CompatibleArrayType& bin) } } +template +auto from_json_object_reserve(ConstructibleObjectType& obj, typename ConstructibleObjectType::size_type size, priority_tag<1> /*unused*/) +-> decltype(obj.reserve(size), void()) +{ + obj.reserve(size); +} + +template +inline void from_json_object_reserve(ConstructibleObjectType& /*obj*/, std::size_t /*size*/, priority_tag<0> /*unused*/) +{} + template::value, int> = 0> inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) @@ -409,6 +420,7 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) ConstructibleObjectType ret; const auto* inner_object = j.template get_ptr(); + from_json_object_reserve(ret, inner_object->size(), priority_tag<1> {}); for (const auto& p : *inner_object) { ret.emplace(p.first, p.second.template get()); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 0610eafee..d3c8293a7 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -5693,6 +5693,17 @@ inline void from_json(const BasicJsonType& j, CompatibleArrayType& bin) } } +template +auto from_json_object_reserve(ConstructibleObjectType& obj, typename ConstructibleObjectType::size_type size, priority_tag<1> /*unused*/) +-> decltype(obj.reserve(size), void()) +{ + obj.reserve(size); +} + +template +inline void from_json_object_reserve(ConstructibleObjectType& /*obj*/, std::size_t /*size*/, priority_tag<0> /*unused*/) +{} + template::value, int> = 0> inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) @@ -5704,6 +5715,7 @@ inline void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) ConstructibleObjectType ret; const auto* inner_object = j.template get_ptr(); + from_json_object_reserve(ret, inner_object->size(), priority_tag<1> {}); for (const auto& p : *inner_object) { ret.emplace(p.first, p.second.template get()); diff --git a/tests/src/unit-conversions.cpp b/tests/src/unit-conversions.cpp index 1937affbb..4975854c0 100644 --- a/tests/src/unit-conversions.cpp +++ b/tests/src/unit-conversions.cpp @@ -1389,6 +1389,37 @@ TEST_CASE("value conversion") // CHECK(m5["one"] == "eins"); } + SECTION("reserve is called on containers that support it (#5406)") + { + // build a larger object so that a missing/incorrect reserve() + // call would be more likely to corrupt or drop elements + json j_large; + for (int i = 0; i < 100; ++i) + { + j_large[std::to_string(i)] = i; + } + + SECTION("std::unordered_map (supports reserve)") + { + const auto m = j_large.get>(); + CHECK(m.size() == 100); + for (int i = 0; i < 100; ++i) + { + CHECK(m.at(std::to_string(i)) == i); + } + } + + SECTION("std::map (no reserve, fallback path)") + { + const auto m = j_large.get>(); + CHECK(m.size() == 100); + for (int i = 0; i < 100; ++i) + { + CHECK(m.at(std::to_string(i)) == i); + } + } + } + SECTION("std::multimap") { j1.get>();