From d1d8966ed2f1a759ea7b12999b6749c65235f00f Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 15 Oct 2026 17:58:44 +0200 Subject: [PATCH] Move object keys into the value when parsing json_sax_dom_parser::key() copied the key from the lexer's buffer into the object. The lexer starts every token with an empty buffer (reset()), so the key can be moved instead, which saves an allocation and a copy for every key that does not fit into the string's inline buffer. json_sax_dom_parser already moved string values the same way. ordered_map has no rvalue operator[], so ordered_json still copies keys. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/input/json_sax.hpp | 5 ++-- single_include/nlohmann/json.hpp | 5 ++-- tests/src/unit-class_parser.cpp | 30 ++++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/include/nlohmann/detail/input/json_sax.hpp b/include/nlohmann/detail/input/json_sax.hpp index 912888324..966610e4a 100644 --- a/include/nlohmann/detail/input/json_sax.hpp +++ b/include/nlohmann/detail/input/json_sax.hpp @@ -418,8 +418,9 @@ class json_sax_dom_parser JSON_ASSERT(!ref_stack.empty()); JSON_ASSERT(ref_stack.back()->is_object()); - // add null at the given key and store the reference for later - object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val)); + // add null at the given key and store the reference for later; the + // key is moved, as the lexer starts the next token with reset() + object_element = &(ref_stack.back()->m_data.m_value.object->operator[](std::move(val))); return true; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 06b0c907a..9cd572355 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -15030,8 +15030,9 @@ class json_sax_dom_parser JSON_ASSERT(!ref_stack.empty()); JSON_ASSERT(ref_stack.back()->is_object()); - // add null at the given key and store the reference for later - object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val)); + // add null at the given key and store the reference for later; the + // key is moved, as the lexer starts the next token with reset() + object_element = &(ref_stack.back()->m_data.m_value.object->operator[](std::move(val))); return true; } diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index c3793f220..1d1b5f40b 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -2452,6 +2452,36 @@ std::string parse_error_message_range(IteratorType first, IteratorType last) } } // namespace +TEST_CASE("object keys are moved into the value") +{ + // json_sax_dom_parser::key() moves the key out of the lexer's buffer into + // the object, and the lexer starts the next token with an empty buffer. + // Keys of every length (inline and allocated strings), keys with escapes, + // and duplicate keys must give the same objects as reading the input one + // character at a time, and as ordered_json, whose map copies the key. + const std::string long_key(100, 'k'); + const std::string doc = R"({"a": 1, ")" + long_key + R"(": {"b": [2, 3], "sixteen_chars_xx": 4}, )" + R"("esc\"apedé": 5, "a": 6, ")" + long_key + R"(x": 7, "": 8, "dup": 9, "dup": 10})"; + + const json j = json::parse(doc); + const std::list chars(doc.begin(), doc.end()); + CHECK(j == json::parse(chars.begin(), chars.end())); + + CHECK(j.size() == 6); + CHECK(j["a"] == 6); + CHECK(j[long_key]["b"] == json({2, 3})); + CHECK(j[long_key]["sixteen_chars_xx"] == 4); + CHECK(j["esc\"aped\xc3\xa9"] == 5); + CHECK(j[long_key + "x"] == 7); + CHECK(j[""] == 8); + CHECK(j["dup"] == 10); + + const auto o = nlohmann::ordered_json::parse(doc); + CHECK(o.size() == 6); + CHECK(o[long_key + "x"] == 7); + CHECK(json(o) == j); +} + TEST_CASE("text of float tokens") { // For contiguous input, the lexer copies the text of a float token into