Files
json/tests/src/unit-json_view_edit.cpp
Niels Lohmann accba4d3b6 Write json_view's dump() without a call or conversion per token
The default dump() (no indentation, no ensure_ascii) gets its own
writer that makes the same walk and produces the same output:

- the write position stays in a local variable instead of a
  member, so the compiler keeps it in a register across stores
  through aliasing char pointers;
- strings and number tokens are copied with fixed-size 32-byte
  moves wherever enough source bytes remain, instead of one
  memcpy call per token;
- the innermost open container lives in local variables; a stack
  that starts as a local array of 32 entries holds the rest;
- unedited documents are walked through the node array in order,
  and integer tokens are read from the source directly.

On top of that, float tokens of at most 15 significant digits are
written straight from their digits via zmij::to_shortest() and
write_shortest(), without converting to a double and back: such
decimals are farther apart than a double's rounding interval, so
the token's digits are the double's shortest digits. Tokens of
16+ digits, or edited values, still go through decimal_to_float().
The view's own NEON write_decimal() is removed in favor of the
shared writer, and the dump output now grows in 64 KiB steps
instead of being resized to its estimate at once.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-07 16:42:54 +02:00

580 lines
23 KiB
C++

// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;
using nlohmann::ordered_json_document;
using nlohmann::ordered_json_editable_document;
using nlohmann::ordered_json_editable_view;
using ptr_t = ordered_json::json_pointer;
#include <array>
#include <cmath>
#include <cstring>
#include <cstdint>
#include <functional>
#include <iterator>
#include <limits>
#include <map>
#include <random>
#include <string>
#include <vector>
namespace
{
std::uint32_t rng()
{
static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
// result_type is std::uint_fast32_t, which may be wider than 32 bits
const std::mt19937::result_type value = generator();
return static_cast<std::uint32_t>(value);
}
int r(int n)
{
return static_cast<int>(rng() % static_cast<unsigned>(n));
}
int counter = 0;
std::uint64_t bits(double x)
{
std::uint64_t b = 0;
std::memcpy(&b, &x, sizeof(b));
return b;
}
std::string random_string()
{
static const std::array<const char*, 15> pieces = {{"a", "Z", " ", "~", "\n", "\"", "\\", "/", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\x1f", "0", "key"}};
std::string s;
for (int i = r(3) == 0 ? r(30) : r(6); i > 0; --i)
{
s += pieces[static_cast<std::size_t>(r(15))];
}
return s;
}
ordered_json random_scalar()
{
switch (r(9))
{
case 0:
return nullptr;
case 1:
return r(2) == 0;
case 2:
return static_cast<std::int64_t>(rng()) - 2147483648LL;
case 3:
return (static_cast<std::uint64_t>(rng()) * 4294967296ULL) + rng();
case 4:
return static_cast<double>(static_cast<std::int32_t>(rng())) / (1 + r(1000));
case 5:
return r(2) == 0 ? 1e300 * (r(2) == 0 ? 1 : -1) : 5e-324;
case 6:
return -0.0;
default:
return random_string();
}
}
ordered_json random_value(int depth)
{
const int k = depth > 3 ? 4 : r(7);
if (k == 0)
{
ordered_json o = ordered_json::object();
for (int i = r(5); i > 0; --i)
{
o[random_string() + "#" + std::to_string(counter++)] = random_value(depth + 1);
}
return o;
}
if (k == 1)
{
ordered_json a = ordered_json::array();
for (int i = r(5); i > 0; --i)
{
a.push_back(random_value(depth + 1));
}
return a;
}
return random_scalar();
}
void collect(const ordered_json& j, const ptr_t& p, std::vector<ptr_t>& out)
{
out.push_back(p);
if (j.is_object())
{
for (const auto& kv : j.items())
{
collect(kv.value(), p / kv.key(), out);
}
}
else if (j.is_array())
{
for (std::size_t i = 0; i < j.size(); ++i)
{
collect(j[i], p / i, out);
}
}
}
// the edited view and the ordered_json value read the same through the whole
// read API
void compare(const ordered_json_editable_view& v, const ordered_json& j)
{
REQUIRE(v.type() == j.type());
REQUIRE(v.size() == j.size());
if (j.is_object())
{
auto jt = j.begin();
for (auto it = v.begin(); it != v.end(); ++it, ++jt)
{
CHECK(std::string(it.key().data(), it.key().size()) == jt.key());
CHECK(v[jt.key()].dump() == jt.value().dump());
CHECK(v.contains(jt.key()));
compare(*it, jt.value());
}
}
else if (j.is_array())
{
for (std::size_t i = 0; i < j.size(); ++i)
{
CHECK(v[i].dump() == j[i].dump());
}
std::size_t i = 0;
for (const auto e : v)
{
compare(e, j[i++]);
}
if (!j.empty())
{
CHECK(v.back().dump() == j.back().dump());
}
}
else if (j.is_string())
{
CHECK(v.get<std::string>() == j.get<std::string>());
}
else if (j.is_number_float())
{
CHECK(bits(v.get<double>()) == bits(j.get<double>()));
}
else if (j.is_number_integer())
{
CHECK(v.get<std::int64_t>() == j.get<std::int64_t>());
CHECK(v.get<std::uint64_t>() == j.get<std::uint64_t>());
}
}
void check_all(const ordered_json_editable_document& d, const ordered_json& j, bool deep)
{
const std::string text = d.root().dump();
REQUIRE(text == j.dump());
CHECK(d.root().materialize() == j);
if (deep)
{
CHECK(d.root().dump(2) == j.dump(2));
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
CHECK(d.root() == j);
compare(d.root(), j);
// a fresh document of the text reads the same
const ordered_json_document fresh = ordered_json_document::parse(text);
CHECK(fresh.root() == d.root());
}
}
} // namespace
TEST_CASE("json_view edits: differential")
{
// random edits are applied to an ordered_json_editable_document and to the
// ordered_json parse() produces; after every edit both must serialize,
// materialize, and read back the same
for (int n = 0; n < 150; ++n)
{
ordered_json j = random_value(0);
if (r(4) == 0)
{
j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}});
}
const std::string text = j.dump(r(2) == 0 ? -1 : 2);
CAPTURE(text)
ordered_json_editable_document d = ordered_json_editable_document::parse(text);
j = ordered_json::parse(text);
const ordered_json_document other = ordered_json_document::parse(random_value(0).dump());
const int edits = 30;
for (int e = 0; e < edits; ++e)
{
std::vector<ptr_t> paths;
collect(j, ptr_t(), paths);
const ptr_t p = paths[static_cast<std::size_t>(r(static_cast<int>(paths.size())))];
const ordered_json& target = j[p];
const ordered_json_editable_view tv = d.root().at(p);
const int op = r(12);
{
if (op == 0) // assign a scalar
{
const ordered_json v = random_scalar();
if (v.is_string() && r(2) == 0)
{
d.set(tv, v.get<std::string>());
}
else if (v.is_number_unsigned() && r(2) == 0)
{
d.set(tv, v.get<std::uint64_t>());
}
else
{
d.set(tv, v);
}
j[p] = v;
}
else if (op == 1) // assign a new array/object (or anything), sometimes via the pointer API
{
const ordered_json v = random_value(2);
if (r(2) == 0)
{
d.set(p, v);
}
else
{
d.set(tv, v);
}
j[p] = v;
}
else if (op == 2) // copy a value of the same document
{
const ptr_t& q = paths[static_cast<std::size_t>(r(static_cast<int>(paths.size())))];
const ordered_json v = j[q];
d.set(tv, d.root().at(q));
j[p] = v;
}
else if (op == 3) // copy a value of another document
{
d.set(tv, other.root());
j[p] = other.root().materialize();
}
else if ((op == 4 || op == 5) && target.is_object()) // set a member (new or existing)
{
std::string key = random_string() + "#" + std::to_string(counter++);
if (op == 5 && !target.empty())
{
key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
}
const ordered_json v = random_value(2);
d.set(tv, key, v);
j[p][key] = v;
}
else if (op == 6 && target.is_object() && !target.empty()) // erase a member
{
const std::string key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
if (r(2) == 0)
{
d.erase(tv, key);
}
else
{
d.erase(p / key);
}
j[p].erase(key);
}
else if (op == 7 && (target.is_array() || target.is_null())) // push_back
{
const ordered_json v = random_value(2);
d.push_back(tv, v);
j[p].push_back(v);
}
else if (op == 8 && target.is_array()) // insert
{
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size()) + 1));
const ordered_json v = random_value(2);
d.insert(tv, i, v);
j[p].insert(j[p].begin() + static_cast<std::ptrdiff_t>(i), v);
}
else if (op == 9 && target.is_array() && !target.empty()) // erase an element
{
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
if (r(2) == 0)
{
d.erase(tv, i);
}
else
{
d.erase(p / i);
}
j[p].erase(i);
}
else if (op == 10 && target.is_array() && !target.empty()) // assign an element
{
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
const ordered_json v = random_value(2);
d.set(tv, i, v);
j[p][i] = v;
}
else if (op == 11) // a held view sees the assignment
{
const ordered_json v = random_value(2);
const ordered_json_editable_view held = d.root().at(p);
d.set(p, v);
j[p] = v;
CHECK(held.dump() == v.dump());
}
else
{
continue;
}
}
CAPTURE(p.to_string())
CAPTURE(op)
check_all(d, j, e % 8 == 7 || e == edits - 1);
}
}
}
namespace
{
#if !defined(JSON_NOEXCEPTION)
// the exception a call throws, or "" if it throws none
std::string exception_of_call(const std::function<void()>& f)
{
try
{
f();
}
catch (const json::exception& e)
{
return e.what();
}
return "";
}
#endif
} // namespace
TEST_CASE("json_view edits: errors")
{
SECTION("an empty document")
{
json_editable_document d;
CHECK_THROWS_WITH_AS(d.set(d.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
}
json_editable_document d = json_editable_document::parse(R"({"o": {"a": 1}, "a": [1, 2], "n": 1, "z": null})");
const json_editable_view root = d.root();
const json_document other = json_document::parse("[1]");
json_editable_document other_editable = json_editable_document::parse("[1]");
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
#if !defined(JSON_NOEXCEPTION)
// invalid UTF-8 is rejected when it enters the document, with the error
// basic_json::dump() reports for the same string
for (const std::string bad :
{"\xC3\x28", "a\xE2\x28\xA1", "\xF0\x28\x8C\xBC", "\xE2\x82", "x\xF0\x9F\x98", "\xFF", "\xED\xA0\x80", "\xC0\xAF"
})
{
CAPTURE(bad)
const std::string expected = exception_of_call([&]
{
const std::string text = json(bad).dump();
static_cast<void>(text);
});
CHECK(!expected.empty());
CHECK(exception_of_call([&] { d.set(root["z"], bad); }) == expected);
CHECK(exception_of_call([&] { d.set(root["o"], bad, 1); }) == expected);
CHECK(exception_of_call([&] { d.set(root["z"], json{{"k", bad}}); }) == expected);
}
#endif
// nothing of the failed edits is visible
CHECK(root.dump() == R"({"o":{"a":1},"a":[1,2],"n":1,"z":null})");
static_cast<void>(other);
}
TEST_CASE("json_view edits: views and values")
{
SECTION("a value that is no longer part of the document")
{
json_editable_document d = json_editable_document::parse("[[[1,2]]]");
const json_editable_view inner = d.root()[0][0];
d.set(d.root()[0], json::array({7}));
d.set(inner, 5);
CHECK(d.root().dump() == "[[7]]");
CHECK(inner.get<int>() == 5);
}
SECTION("views keep referring to their value")
{
json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})");
const json_editable_view a = d.root()["a"];
const json_editable_view twenty = a[1];
const json_editable_view c = d.root()["b"]["c"];
d.insert(a, 0, 5);
d.push_back(a, 40);
CHECK(twenty.get<int>() == 20);
CHECK(a[2].get<int>() == 20);
d.erase(a, 2);
CHECK(twenty.get<int>() == 20); // an erased value keeps its last value
d.set(c, 7);
CHECK(c.get<int>() == 7); // a held view sees an assignment
d.set(d.root()["b"], json::array({1, 2}));
CHECK(d.root()["b"].dump() == "[1,2]");
CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})");
CHECK(d.root()["a"][0].source_offset() == static_cast<std::size_t>(-1)); // a new value
CHECK(d.root()["a"][1].source_offset() != static_cast<std::size_t>(-1));
}
SECTION("strings stay valid while more edits come")
{
json_editable_document d = json_editable_document::parse("[]");
const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string();
for (int i = 0; i < 1000; ++i)
{
d.push_back(d.root(), std::string(static_cast<std::size_t>(i % 50), 'y'));
}
CHECK(std::string(first.data(), first.size()) == std::string(100, 'x'));
CHECK(d.root().size() == 1001);
}
SECTION("numbers")
{
json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])");
d.set(d.root(), 2, 0.1);
d.push_back(d.root(), std::numeric_limits<double>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<double>::infinity());
d.push_back(d.root(), (std::numeric_limits<std::uint64_t>::max)());
d.push_back(d.root(), (std::numeric_limits<std::int64_t>::min)());
CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]");
CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]");
CHECK(std::isnan(d.root()[3].get<double>()));
CHECK(std::isinf(d.root()[4].get<double>()));
CHECK(d.root()[2].number_token() == "0.1");
CHECK(d.root()[5].get<std::uint64_t>() == 18446744073709551615u);
CHECK(d.root()[6].number_token() == "-9223372036854775808");
CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump());
}
SECTION("numbers of other float types")
{
// doubles have their own path to the output; other float types are
// written as basic_json writes them, non-finite values as null
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
using document_float = nlohmann::basic_json_document<json_float, true>;
document_float d = document_float::parse("[1.5]");
d.push_back(d.root(), std::numeric_limits<float>::quiet_NaN());
d.push_back(d.root(), -std::numeric_limits<float>::infinity());
CHECK(d.root().dump() == "[1.5,null,null]");
CHECK(d.root().dump(2) == json_float::parse("[1.5, null, null]").dump(2));
}
SECTION("nulls become containers, and the root can be replaced")
{
json_editable_document d = json_editable_document::parse("[null, null]");
d.set(d.root()[0], "k", 1);
d.push_back(d.root()[1], true);
CHECK(d.root().dump() == R"([{"k":1},[true]])");
d.set(d.root(), "scalar");
CHECK(d.root().dump() == R"("scalar")");
d.set(d.root(), json{{"x", {1, 2}}});
d.set(json::json_pointer("/x/-"), 3);
d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too
d.set(json::json_pointer("/y"), false);
CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})");
CHECK(d.erase(json::json_pointer("/x/0")) == 1);
CHECK(d.erase(json::json_pointer("/y")) == 1);
CHECK(d.erase(json::json_pointer("/nothing")) == 0);
CHECK(d.root().dump() == R"({"x":[2,3,4]})");
}
SECTION("duplicate keys")
{
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
d.set(d.root(), "a", 4); // the first member is assigned, the others dropped
CHECK(d.root().dump() == R"({"a":4,"b":2})");
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
CHECK(d.erase(d.root(), "a") == 2);
CHECK(d.root().dump() == R"({"b":2})");
}
SECTION("values from other documents")
{
const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})");
json_editable_document edited = json_editable_document::parse("[0]");
edited.set(edited.root(), 0, json{{"inner", {1, 2}}});
json_editable_document d = json_editable_document::parse("{}");
d.set(d.root(), "copy", source.root()["list"]);
d.set(d.root(), "text", source.root()["text"]);
d.set(d.root(), "edited", edited.root()[0]);
d.set(d.root(), "self", d.root()["copy"]);
CHECK(d.root().dump() == R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})");
CHECK(d.root()["copy"] == source.root()["list"]);
CHECK(source.root()["list"] == d.root()["self"]);
CHECK(d.root() != source.root());
}
SECTION("large objects")
{
std::string text = "{";
for (int i = 0; i < 200; ++i)
{
text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i);
}
text += '}';
json_editable_document d = json_editable_document::parse(text);
d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use
CHECK(d.root()["k7"].get_string() == "seven");
d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear
CHECK(d.root()["new"].get<int>() == 1);
CHECK(d.root()["k199"].get<int>() == 199);
d.erase(d.root(), "k0");
CHECK(!d.root().contains("k0"));
CHECK(d.root().size() == 200);
}
SECTION("reuse and memory")
{
json_editable_document d = json_editable_document::parse("[1, 2, 3]");
const std::size_t before = d.memory_usage();
for (int i = 0; i < 100; ++i)
{
d.push_back(d.root(), "some text");
}
CHECK(d.memory_usage() > before);
const json_editable_view first = d.root()[0];
d.shrink_to_fit(); // (with edits, the index stays in place)
CHECK(first.get<int>() == 1);
d.read(std::string("[true]"));
CHECK(d.root().dump() == "[true]");
d.push_back(d.root(), false);
CHECK(d.root().dump() == "[true,false]");
}
}