From ad31938c71649f4e5b06429f9b5be7bb681ca1c3 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 15 Oct 2026 08:16:26 +0200 Subject: [PATCH] Make flatten() linear for ordered_json Signed-off-by: Niels Lohmann --- include/nlohmann/detail/json_pointer.hpp | 30 ++++++++++++-- include/nlohmann/ordered_map.hpp | 11 +++++ single_include/nlohmann/json.hpp | 41 +++++++++++++++++-- tests/src/unit-json_pointer.cpp | 52 ++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 1b439f9bf..68c696ba0 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -938,6 +938,23 @@ class json_pointer } private: + // Add the entry (key, v) to the object `obj`. Flattened paths are pairwise + // distinct, so objects that can append a key known to be absent + // (ordered_map) skip the lookup, which is linear and would make flatten + // quadratic; all other object types use operator[]. + template + static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, int /*unused*/) + -> decltype(obj.emplace_new_key(key, std::forward(v)), void()) + { + obj.emplace_new_key(key, std::forward(v)); + } + + template + static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, ...) + { + obj[key] = std::forward(v); + } + /*! @param[in] reference_string the reference string to the current value @param[in] value the value to consider @@ -982,7 +999,8 @@ class json_pointer // flatten `v`, whose path is `path`: primitives and empty containers // are added to the result right away; other containers get a frame - const auto enter = [&stack, &path, &result](const BasicJsonType & v) + auto& object = *result.m_data.m_value.object; + const auto enter = [&stack, &path, &object](const BasicJsonType & v) { switch (v.type()) { @@ -991,7 +1009,7 @@ class json_pointer if (v.m_data.m_value.array->empty()) { // flatten empty array as null - result[path] = nullptr; + add_flattened(object, path, nullptr, 0); } else { @@ -1005,7 +1023,7 @@ class json_pointer if (v.m_data.m_value.object->empty()) { // flatten empty object as null - result[path] = nullptr; + add_flattened(object, path, nullptr, 0); } else { @@ -1025,7 +1043,7 @@ class json_pointer default: { // add a primitive value with its reference string - result[path] = v; + add_flattened(object, path, v, 0); return; } } @@ -1071,6 +1089,10 @@ class json_pointer enter(it->second); } } + + // the entries were added without going through basic_json, so the + // parent pointers (JSON_DIAGNOSTICS) have to be set now + result.set_parents(); } /*! diff --git a/include/nlohmann/ordered_map.hpp b/include/nlohmann/ordered_map.hpp index fdcf4a5c0..ef27bbe45 100644 --- a/include/nlohmann/ordered_map.hpp +++ b/include/nlohmann/ordered_map.hpp @@ -143,6 +143,17 @@ public: return {std::prev(this->end()), true}; } + /// @brief add an element at the end without checking whether the key exists + /// @note The caller must guarantee that @a key is not yet contained, otherwise + /// the container holds duplicate keys. This avoids the linear search of + /// emplace() and operator[] when many keys known to be new are added. + template::value, int> = 0> + void emplace_new_key(const key_type& key, V && t) + { + append(key, std::forward(t)); + } + T& operator[](const key_type& key) { return emplace(key, T{}).first->second; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index cd5189628..48d7a3084 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21562,6 +21562,23 @@ class json_pointer } private: + // Add the entry (key, v) to the object `obj`. Flattened paths are pairwise + // distinct, so objects that can append a key known to be absent + // (ordered_map) skip the lookup, which is linear and would make flatten + // quadratic; all other object types use operator[]. + template + static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, int /*unused*/) + -> decltype(obj.emplace_new_key(key, std::forward(v)), void()) + { + obj.emplace_new_key(key, std::forward(v)); + } + + template + static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, ...) + { + obj[key] = std::forward(v); + } + /*! @param[in] reference_string the reference string to the current value @param[in] value the value to consider @@ -21606,7 +21623,8 @@ class json_pointer // flatten `v`, whose path is `path`: primitives and empty containers // are added to the result right away; other containers get a frame - const auto enter = [&stack, &path, &result](const BasicJsonType & v) + auto& object = *result.m_data.m_value.object; + const auto enter = [&stack, &path, &object](const BasicJsonType & v) { switch (v.type()) { @@ -21615,7 +21633,7 @@ class json_pointer if (v.m_data.m_value.array->empty()) { // flatten empty array as null - result[path] = nullptr; + add_flattened(object, path, nullptr, 0); } else { @@ -21629,7 +21647,7 @@ class json_pointer if (v.m_data.m_value.object->empty()) { // flatten empty object as null - result[path] = nullptr; + add_flattened(object, path, nullptr, 0); } else { @@ -21649,7 +21667,7 @@ class json_pointer default: { // add a primitive value with its reference string - result[path] = v; + add_flattened(object, path, v, 0); return; } } @@ -21695,6 +21713,10 @@ class json_pointer enter(it->second); } } + + // the entries were added without going through basic_json, so the + // parent pointers (JSON_DIAGNOSTICS) have to be set now + result.set_parents(); } /*! @@ -29161,6 +29183,17 @@ public: return {std::prev(this->end()), true}; } + /// @brief add an element at the end without checking whether the key exists + /// @note The caller must guarantee that @a key is not yet contained, otherwise + /// the container holds duplicate keys. This avoids the linear search of + /// emplace() and operator[] when many keys known to be new are added. + template::value, int> = 0> + void emplace_new_key(const key_type& key, V && t) + { + append(key, std::forward(t)); + } + T& operator[](const key_type& key) { return emplace(key, T{}).first->second; diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 658a7fa8d..cff7fe8c2 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -24,6 +24,8 @@ using nlohmann::json; #endif #include +#include +#include #include TEST_CASE("JSON pointers") @@ -1071,3 +1073,53 @@ TEST_CASE("flatten of structured values") R"({"/z":"s","/a~1b/~0":1,"/a~1b/k":null,"/a/0":1,"/a/1/c":null,"/a/1/b":null,"/a/2/0/x~0~1/0":true,"/a/2/0/x~0~1/1":null,"/a/2/0/w":2})"); } } + +TEST_CASE("flatten of ordered_json") +{ + using nlohmann::ordered_json; + + SECTION("keeps the document order of the leaves") + { + const ordered_json j = ordered_json::parse(R"({ + "z": 1, "a": {"y": [true, null, {}], "b": "x", "": []}, + "m~n": 2.5, "o/p": {"k": {}}, "0": [[1], [2, 3]] + })"); + + const ordered_json flat = j.flatten(); + std::vector keys; + for (const auto& el : flat.items()) + { + keys.push_back(el.key()); + } + const std::vector expected = + { + "/z", "/a/y/0", "/a/y/1", "/a/y/2", "/a/b", "/a/", "/m~0n", "/o~1p/k", "/0/0/0", "/0/1/0", "/0/1/1" + }; + CHECK(keys == expected); + CHECK(flat.size() == expected.size()); + CHECK(flat["/m~0n"] == 2.5); + CHECK(flat["/a/y/2"].is_null()); + } + + SECTION("round-trips and agrees with json") + { + ordered_json j; + for (int i = 0; i < 200; ++i) + { + j["k" + std::to_string(i)] = {{"v", i}, {"l", {i, i + 1, "s"}}}; + } + const ordered_json flat = j.flatten(); + CHECK(flat.size() == 800); + CHECK(flat.unflatten() == j); + + // same entries as for a sorted object + const nlohmann::json plain = nlohmann::json::parse(j.dump()); + CHECK(nlohmann::json::parse(flat.dump()) == plain.flatten()); + } + + SECTION("primitive and empty root") + { + CHECK(ordered_json(1).flatten() == ordered_json({{"", 1}})); + CHECK(ordered_json::array().flatten() == ordered_json({{"", nullptr}})); + } +}