Compare commits

...
3 Commits
Author SHA1 Message Date
Niels Lohmann 47d84516eb Merge branch 'develop' into json-view/5869-7-check-utf8
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 07:29:14 +01:00
Niels Lohmann 2bb2feeef4 Fix the red CI on develop (json_view pointer tokens, bson, from_json tests, Infer, IWYU) (#5942)
* json_view: pointer tokens that cannot be array indices create objects

editable_document::set() chose an array for a null parent whenever the
token consisted of digits, so "01" threw parse_error.106 and the empty
token threw, while basic_json::operator[](json_pointer) has created an
object for such tokens since #5358. This broke
unit-json_view_edit_values on develop. Use the same RFC 6901 array index
test as basic_json.

Also re-indent the unflatten test that the astyle check flagged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix the other red jobs on develop

- unit-bson: initialize recorder::events in its declaration (-Weffc++)
- unit-json_view_from_json: suppress -Wexit-time-destructors for the
  serialize-enum tables and -Wfloat-equal for same()
- unit-pointer_access: @infer-ignore NULLPTR_DEREFERENCE for the
  dereferences after get_ptr() that Infer reports
- json_view.hpp: IWYU no_include for the detail headers that the view
  code uses through nlohmann/json.hpp

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix GCC warnings in the json_view from_json test

GCC (-Weffc++, -Wfloat-equal) rejected unit-json_view_from_json.cpp
(ci_test_standards_gcc (20)): initialize the members of shape and
with_names, and suppress -Wfloat-equal for GCC like for clang.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 07:27:34 +01:00
Niels Lohmann 21ec9bd801 json_view: fewer allocations when parsing small documents (#5915)
- Size the buffer of decoded strings on its first growth from the input (a
  quarter of it plus 16 bytes, at most 4 KiB) instead of 272 bytes, so that a
  document with escapes does not grow it twice, with a zero-filled copy each
  time.
- Allocate the node index of documents with an estimate of up to 1024 nodes
  (was 256) together with the header, which saves one allocation. When the
  estimate is too low (dense numbers), that block stays unused: at most 16 KiB
  instead of 4 KiB. shrink_to_fit() cannot release it separately; the
  documentation says so.

Fresh parse of a 2.5 KB minified tweet: 993 to 958 ns (-3.5 %), traverse and
select -3 %, with no row consistently slower (Apple M1 Max, 30 rounds, three
runs). Dense numeric documents parse about 9 % faster and keep up to 16 KiB
more memory.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-27 07:22:49 +01:00
10 changed files with 131 additions and 22 deletions

No files matched your search

@@ -23,6 +23,10 @@ Linear in [`node_count()`](node_count.md) plus the length of the decoded strings
## Notes
The node index of a small document (an estimate of up to 1024 nodes, which is up to about 4 KiB of minified text) is
allocated together with the document's header, so that parsing needs one allocation. That part cannot be released
separately: `shrink_to_fit()` only trims a node index that has its own allocation, and the decoded strings.
!!! warning "Invalidates views"
Unlike moving the document, `shrink_to_fit()` **invalidates every view taken from this document before the
+9 -1
View File
@@ -543,7 +543,15 @@ run_found:
NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n)
{
const std::size_t used = arena_used();
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, used + n + 256));
std::size_t wanted = used + n + 256;
if (m_doc.arena.empty())
{
// the first growth: a decoded string is never longer than its source,
// so a fraction of the input (capped, as the buffer is zero-filled)
// covers the escapes of a typical document without regrowing it
wanted = (std::max)(wanted, (std::min)((static_cast<std::size_t>(m_end - m_begin) / 4) + 16, std::size_t{4096}));
}
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, wanted));
m_arena_write = m_doc.arena.data() + used;
m_arena_end = m_doc.arena.data() + m_doc.arena.size();
}
@@ -82,14 +82,17 @@ struct document_data
};
std::unique_ptr<edit_state> edits; ///< created by the first edit
/// the most nodes that are allocated together with the header (16 KiB)
static constexpr std::size_t max_inline_nodes = 1024;
/// One allocation for the header and room for `count` nodes, so that a
/// small document needs one allocation instead of two. Large documents get
/// a separate node array instead (so it can be trimmed). When a document
/// outgrows the inline array, that array stays unused: at most 256 nodes
/// (4 KiB), and only for documents whose estimate was too small.
/// outgrows the inline array, that array stays unused: at most 1024 nodes
/// (16 KiB), and only for documents whose estimate was too small.
static document_data* create(std::size_t count)
{
count = count <= 256 ? count : 0;
count = count <= max_inline_nodes ? count : 0;
void* mem = ::operator new (sizeof(document_data) + (count * sizeof(node)));
auto* d = new (mem) document_data(); // NOLINT(cppcoreguidelines-owning-memory): owned by the returned pointer, freed by deleter
// (aligned: sizeof is a multiple of the alignment; through void*, as GCC's -Wcast-align wants)
+8 -2
View File
@@ -54,9 +54,11 @@
// IWYU pragma: no_include <version>
// IWYU pragma: no_include "nlohmann/detail/abi_config.hpp"
// IWYU pragma: no_include "nlohmann/detail/abi_macros.hpp"
// IWYU pragma: no_include "nlohmann/detail/conversions/from_json.hpp"
// IWYU pragma: no_include "nlohmann/detail/input/input_adapters.hpp"
// IWYU pragma: no_include "nlohmann/detail/json_pointer.hpp"
// IWYU pragma: no_include "nlohmann/detail/meta/cpp_future.hpp"
// IWYU pragma: no_include "nlohmann/detail/meta/type_traits.hpp"
// IWYU pragma: no_include "nlohmann/detail/string_concat.hpp"
// IWYU pragma: no_include "nlohmann/detail/value_t.hpp"
// IWYU pragma: no_include "nlohmann/json.hpp"
@@ -1313,7 +1315,7 @@ class basic_json_document
/// member is set (added if missing), an array element assigned, and "-"
/// or the size of the array appends. A null parent becomes what
/// basic_json's operator[](json_pointer) makes of it: an array for "-"
/// and for digits (padded with nulls up to the index), an object otherwise.
/// and for array indices (padded with nulls up to the index), an object otherwise.
template<typename V>
view_type set(const json_pointer& ptr, V&& value)
{
@@ -1329,7 +1331,11 @@ class basic_json_document
{
return push_back(parent, std::forward<V>(value));
}
const bool digits = std::all_of(token.begin(), token.end(), [](const char c)
// (a token that cannot be an array index, such as "01" or "", names an
// object member, as in basic_json)
const bool digits = !token.empty()
&& (token.size() == 1 || token[0] != '0')
&& std::all_of(token.begin(), token.end(), [](const char c)
{
return c >= '0' && c <= '9';
});
+23 -6
View File
@@ -54,9 +54,11 @@
// IWYU pragma: no_include <version>
// IWYU pragma: no_include "nlohmann/detail/abi_config.hpp"
// IWYU pragma: no_include "nlohmann/detail/abi_macros.hpp"
// IWYU pragma: no_include "nlohmann/detail/conversions/from_json.hpp"
// IWYU pragma: no_include "nlohmann/detail/input/input_adapters.hpp"
// IWYU pragma: no_include "nlohmann/detail/json_pointer.hpp"
// IWYU pragma: no_include "nlohmann/detail/meta/cpp_future.hpp"
// IWYU pragma: no_include "nlohmann/detail/meta/type_traits.hpp"
// IWYU pragma: no_include "nlohmann/detail/string_concat.hpp"
// IWYU pragma: no_include "nlohmann/detail/value_t.hpp"
// IWYU pragma: no_include "nlohmann/json.hpp"
@@ -410,14 +412,17 @@ struct document_data
};
std::unique_ptr<edit_state> edits; ///< created by the first edit
/// the most nodes that are allocated together with the header (16 KiB)
static constexpr std::size_t max_inline_nodes = 1024;
/// One allocation for the header and room for `count` nodes, so that a
/// small document needs one allocation instead of two. Large documents get
/// a separate node array instead (so it can be trimmed). When a document
/// outgrows the inline array, that array stays unused: at most 256 nodes
/// (4 KiB), and only for documents whose estimate was too small.
/// outgrows the inline array, that array stays unused: at most 1024 nodes
/// (16 KiB), and only for documents whose estimate was too small.
static document_data* create(std::size_t count)
{
count = count <= 256 ? count : 0;
count = count <= max_inline_nodes ? count : 0;
void* mem = ::operator new (sizeof(document_data) + (count * sizeof(node)));
auto* d = new (mem) document_data(); // NOLINT(cppcoreguidelines-owning-memory): owned by the returned pointer, freed by deleter
// (aligned: sizeof is a multiple of the alignment; through void*, as GCC's -Wcast-align wants)
@@ -1747,7 +1752,15 @@ run_found:
NLOHMANN_VIEW_NOINLINE void arena_grow(std::size_t n)
{
const std::size_t used = arena_used();
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, used + n + 256));
std::size_t wanted = used + n + 256;
if (m_doc.arena.empty())
{
// the first growth: a decoded string is never longer than its source,
// so a fraction of the input (capped, as the buffer is zero-filled)
// covers the escapes of a typical document without regrowing it
wanted = (std::max)(wanted, (std::min)((static_cast<std::size_t>(m_end - m_begin) / 4) + 16, std::size_t{4096}));
}
m_doc.arena.resize((std::max)(m_doc.arena.size() * 2, wanted));
m_arena_write = m_doc.arena.data() + used;
m_arena_end = m_doc.arena.data() + m_doc.arena.size();
}
@@ -9105,7 +9118,7 @@ class basic_json_document
/// member is set (added if missing), an array element assigned, and "-"
/// or the size of the array appends. A null parent becomes what
/// basic_json's operator[](json_pointer) makes of it: an array for "-"
/// and for digits (padded with nulls up to the index), an object otherwise.
/// and for array indices (padded with nulls up to the index), an object otherwise.
template<typename V>
view_type set(const json_pointer& ptr, V&& value)
{
@@ -9121,7 +9134,11 @@ class basic_json_document
{
return push_back(parent, std::forward<V>(value));
}
const bool digits = std::all_of(token.begin(), token.end(), [](const char c)
// (a token that cannot be an array index, such as "01" or "", names an
// object member, as in basic_json)
const bool digits = !token.empty()
&& (token.size() == 1 || token[0] != '0')
&& std::all_of(token.begin(), token.end(), [](const char c)
{
return c >= '0' && c <= '9';
});
+1 -1
View File
@@ -1311,7 +1311,7 @@ TEST_CASE("BSON declared document size bounds the content")
// records the events that a SAX parser receives
struct recorder : nlohmann::json_sax<json>
{
std::string events;
std::string events{};
bool null() override
{
+4 -4
View File
@@ -1122,17 +1122,17 @@ TEST_CASE("flatten of structured values")
// the same for other keys that are not valid array indices
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1a", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token '1a'", json::out_of_range&);
"[json.exception.out_of_range.404] unresolved reference token '1a'", json::out_of_range&);
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/-", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&);
"[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&);
}
SECTION("unflatten with the empty token among array indices")
{
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/", 2}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1", 2}, {"/a/", 3}}).unflatten(),
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
// without an index "0", the empty token is an ordinary object key
CHECK(json({{"/a/", 2}}).unflatten() == json({{"a", {{"", 2}}}}));
+55
View File
@@ -666,6 +666,61 @@ TEST_CASE("json_view")
CHECK(small.root().materialize() == json::parse("[1,[2,3],{\"a\":\"b\\n\"}]"));
}
SECTION("block sizes")
{
// documents whose node estimate lies around the size that is allocated
// with the header, and whose decoded strings need the first growth of
// the buffer, in all sizes around the points where the policies change
for (const int count :
{
1, 2, 10, 50, 100, 255, 256, 257, 500, 1000, 1023, 1024, 1025, 2000, 5000
})
{
// strings with escapes (decoded text, minified)
std::string escaped = "[";
for (int i = 0; i < count; ++i)
{
escaped += (i != 0 ? ",\"a\\n" : "\"a\\n") + std::to_string(i) + "\\u00e4\"";
}
escaped += ']';
json_document e = json_document::parse(escaped);
CHECK(e.root().materialize() == json::parse(escaped));
e.shrink_to_fit();
CHECK(e.root().materialize() == json::parse(escaped));
// dense numbers (more nodes per byte than the estimate expects)
std::string dense = "[";
for (int i = 0; i < count; ++i)
{
dense += (i != 0 ? ",1" : "1");
}
dense += ']';
json_document n = json_document::parse(dense);
CHECK(n.node_count() == static_cast<std::size_t>(count) + 1);
CHECK(n.root().materialize() == json::parse(dense));
n.shrink_to_fit();
CHECK(n.root().size() == static_cast<std::size_t>(count));
// the same documents, pretty-printed
const std::string pretty = json::parse(escaped).dump(2);
json_document p = json_document::parse(pretty);
CHECK(p.root().materialize() == json::parse(escaped));
}
// a large document with a single escape does not need a buffer of the
// size of its input
std::string large = R"(["a\n")";
for (int i = 0; i < 100000; ++i)
{
large += ",1";
}
large += ']';
const json_document l = json_document::parse(large);
CHECK(l.root().size() == 100001);
CHECK(l.root()[0].get_string() == "a\n");
CHECK(l.root()[100000].get<int>() == 1);
}
SECTION("source offsets")
{
const std::string text = R"( {"key": "value", "escaped": "a\nb", "n": 42})";
+16 -5
View File
@@ -41,6 +41,13 @@ using nlohmann::ordered_json_document;
#include <optional>
#endif
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal")
DOCTEST_GCC_SUPPRESS_WARNING_PUSH
DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal")
// The from_json() functions of the NLOHMANN_DEFINE_TYPE_* and
// NLOHMANN_JSON_SERIALIZE_ENUM macros read views directly; other from_json()
// functions get the value of materialize().
@@ -61,10 +68,11 @@ struct point
struct shape
{
std::string name;
std::vector<point> points;
point center;
std::map<std::string, point> named;
// initialized for GCC -Weffc++
std::string name{};
std::vector<point> points{};
point center{};
std::map<std::string, point> named{};
NLOHMANN_DEFINE_TYPE_INTRUSIVE(shape, name, points, center, named)
};
@@ -79,7 +87,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(with_defaults, a, b, c)
struct with_names
{
int value = 0;
std::string text;
std::string text{}; // initialized for GCC -Weffc++
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_NAMES(with_names, "Value", value, "Text", text)
@@ -447,3 +455,6 @@ TEST_CASE("json_view from_json")
}
#endif
}
DOCTEST_CLANG_SUPPRESS_WARNING_POP
DOCTEST_GCC_SUPPRESS_WARNING_POP
+5
View File
@@ -141,6 +141,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
const test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -170,6 +171,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -199,6 +201,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
const test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();
@@ -232,6 +235,7 @@ TEST_CASE("pointer access")
const test_type* p2 = value.get_ptr<const test_type*>();
CHECK(p2 == value.get_ptr<const test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p2 == value.get<test_type>());
const test_type* const p3 = value.get_ptr<const test_type* const>();
@@ -257,6 +261,7 @@ TEST_CASE("pointer access")
// check if pointers are returned correctly
const test_type* p1 = value.get_ptr<test_type*>();
CHECK(p1 == value.get_ptr<test_type*>());
// @infer-ignore NULLPTR_DEREFERENCE
CHECK(*p1 == value.get<test_type>());
const test_type* p2 = value.get_ptr<const test_type*>();