From fa5ecbae53fd32b77491c2b817024b03fff9b588 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 161568ded..741793bdd 100644 --- a/include/nlohmann/detail/input/json_sax.hpp +++ b/include/nlohmann/detail/input/json_sax.hpp @@ -411,8 +411,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 ccd99cfb2..d69ea7a8e 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -14600,8 +14600,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