Files
json/tests/src/unit-json_view_from_json.cpp
T
Niels Lohmann 9e67bfa115 json_view: the library's std conversions read views directly (#5923)
* json_view: the library's std conversions read views directly

The library's from_json() overloads (containers, maps, std::array, C
arrays, pairs, tuples, optional, valarray, forward_list, enums, binary,
paths, and scalars) accept views: their traits use the view's
basic_json type, and the few accesses that differ (get_ptr) go through
small helpers. from_json() of a view now finds them; only the probe
still looks up by ADL only. get<T>() of a view thus converts these types
without materialize(), with the same values and exceptions; from an
object, a repeated key keeps the last value, as parse() does.

The view keeps its direct paths for vectors of the types it converts
directly (faster than from_json()) and for containers of views; its
other vector and map paths are replaced by from_json(). For basic_json,
std::valarray and std::forward_list are read in one pass (40 % and 3.5 %
fewer instructions); all other conversions are unchanged.

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

* json_view: enable_if_not_view_t instead of '!' in the from_json() templates

astyle spaces out the template argument list when it contains a negation;
the alias spells the condition with negation<> and leaves the lines alone.

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

* json_view: compact the template parameter lists astyle had spaced out

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-26 20:13:31 +01:00

450 lines
19 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::json_document;
using nlohmann::json_view;
using nlohmann::json_editable_document;
using nlohmann::ordered_json_document;
#include "json_view_test_helpers.hpp"
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
#endif
#include <array>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <forward_list>
#include <list>
#include <map>
#include <set>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <valarray>
#include <vector>
#ifdef JSON_HAS_CPP_17
#include <optional>
#endif
// 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().
namespace view_from_json
{
struct point
{
int x = 0;
int y = 0;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(point, x, y)
friend bool operator==(const point& a, const point& b)
{
return a.x == b.x && a.y == b.y;
}
};
struct shape
{
std::string name;
std::vector<point> points;
point center;
std::map<std::string, point> named;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(shape, name, points, center, named)
};
struct with_defaults
{
int a = 1;
std::string b = "default";
std::vector<int> c = {1, 2};
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(with_defaults, a, b, c)
struct with_names
{
int value = 0;
std::string text;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_NAMES(with_names, "Value", value, "Text", text)
struct empty
{
NLOHMANN_DEFINE_TYPE_INTRUSIVE(empty)
};
enum class color
{
red,
green,
blue
};
NLOHMANN_JSON_SERIALIZE_ENUM(color,
{
{color::red, "red"},
{color::green, "green"},
{color::blue, nullptr},
})
enum class strict_color
{
red,
green
};
NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(strict_color,
{
{strict_color::red, "red"},
{strict_color::green, 1},
})
struct base
{
int id = 0;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(base, id)
};
struct derived : base
{
color c = color::red;
NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(derived, base, c)
};
struct derived_with_defaults : base
{
std::string label = "none";
NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT(derived_with_defaults, base, label)
};
// a base whose from_json() is written for json only: the derived type reads
// its own members from the view and the base part from materialize()
struct legacy_base
{
int id = 0;
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
inline void from_json(const json& j, legacy_base& b)
{
b.id = j.at("id").get<int>();
}
struct derived_from_legacy : legacy_base
{
int extra = 0;
};
NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(derived_from_legacy, legacy_base, extra)
// from_json() for json only: converted through materialize()
struct legacy
{
int v = 0;
};
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
inline void from_json(const json& j, legacy& l)
{
l.v = j.at("v").get<int>();
}
// an unconstrained from_json() template, as in the documentation of
// arbitrary type conversions: converted through materialize()
struct unconstrained
{
int v = 0;
};
template<typename BasicJsonType>
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
void from_json(const BasicJsonType& j, unconstrained& u)
{
u.v = j.at("v").template get<int>();
}
// a from_json() constrained by the user to views and json: reads the view
struct constrained
{
int v = 0;
};
template < typename J, typename std::enable_if < nlohmann::is_json_view<J>::value || std::is_same<J, json>::value, int >::type = 0 >
// NOLINTNEXTLINE(misc-use-internal-linkage): found by ADL, like the from_json() functions of users
void from_json(const J& j, constrained& c)
{
c.v = j.at("v").template get<int>();
}
} // namespace view_from_json
namespace
{
template<typename T, typename View = json_view>
struct reads_view : nlohmann::detail::has_view_from_json<nlohmann::adl_serializer<T, void>, View, T> {};
// whether a member converts to the same T from the view and from the materialized value
template<typename T>
bool same(const json_view& v, const json& j, const char* key)
{
return v[key].get<T>() == j[key].get<T>();
}
} // namespace
TEST_CASE("json_view from_json")
{
using namespace view_from_json; // NOLINT(google-build-using-namespace)
SECTION("which from_json() functions read views")
{
static_assert(reads_view<point>::value, "macro types read views");
static_assert(reads_view<shape>::value, "macro types read views");
static_assert(reads_view<with_defaults>::value, "macro types read views");
static_assert(reads_view<with_names>::value, "macro types read views");
static_assert(reads_view<empty>::value, "macro types read views");
static_assert(reads_view<derived>::value, "macro types read views");
static_assert(reads_view<derived_from_legacy>::value, "macro types read views");
static_assert(reads_view<color>::value, "enum macro types read views");
static_assert(reads_view<strict_color>::value, "enum macro types read views");
static_assert(reads_view<constrained>::value, "constrained from_json() reads views");
static_assert(reads_view<point, nlohmann::json_editable_view>::value, "editable views as well");
static_assert(reads_view<point, nlohmann::ordered_json_view>::value, "views of ordered_json as well");
static_assert(!reads_view<legacy>::value, "from_json() for json");
static_assert(!reads_view<legacy_base>::value, "from_json() for json");
static_assert(!reads_view<unconstrained>::value, "unconstrained from_json()");
static_assert(nlohmann::is_json_view<json_view>::value, "is_json_view");
static_assert(nlohmann::is_json_view<nlohmann::ordered_json_editable_view>::value, "is_json_view");
static_assert(!nlohmann::is_json_view<json>::value, "is_json_view");
static_assert(!nlohmann::is_json_view<json_document>::value, "is_json_view");
static_assert(!nlohmann::is_json_view<int>::value, "is_json_view");
}
SECTION("the library's own conversions read views")
{
static_assert(reads_view<std::list<int>>::value, "std types");
static_assert(reads_view<std::deque<point>>::value, "std types");
static_assert(reads_view<std::set<std::string>>::value, "std types");
static_assert(reads_view<std::unordered_set<int>>::value, "std types");
static_assert(reads_view<std::forward_list<int>>::value, "std types");
static_assert(reads_view<std::valarray<double>>::value, "std types");
static_assert(reads_view<std::array<int, 3>>::value, "std types");
static_assert(reads_view<std::pair<int, std::string>>::value, "std types");
static_assert(reads_view<std::tuple<int, bool, point>>::value, "std types");
static_assert(reads_view<std::map<int, int>>::value, "std types");
static_assert(reads_view<std::unordered_map<int, point>>::value, "std types");
static_assert(reads_view<std::map<color, int>>::value, "std types");
static_assert(reads_view<std::map<std::string, int>>::value, "std types");
static_assert(reads_view<std::vector<point>>::value, "std types");
static_assert(reads_view<std::vector<bool>>::value, "std types");
static_assert(reads_view<std::vector<std::uint8_t>>::value, "std types");
static_assert(reads_view<std::string>::value, "std types");
static_assert(reads_view<int>::value, "std types");
static_assert(reads_view<bool>::value, "std types");
static_assert(reads_view<double>::value, "std types");
static_assert(reads_view<std::nullptr_t>::value, "std types");
static_assert(!reads_view<json>::value, "get<BasicJsonType>() is materialize()");
static_assert(reads_view<json::binary_t>::value, "std types");
#ifdef JSON_HAS_CPP_17
static_assert(reads_view<std::optional<point>>::value, "std types");
#endif
CHECK(true);
}
SECTION("std types: the same values as from the materialized value")
{
const json_document d = json_document::parse(R"({"ints": [3, 1, 2, 1], "strings": ["b", "a", "b"], "pairs": [[1, "x"], [2, "y"]],
"points": [{"x": 1, "y": 2}, {"x": 3, "y": 4}], "object": {"b": 1, "a": 2}, "triple": [1, true, {"x": 5, "y": 6}],
"colors": {"green": 1, "red": 2}, "bytes": [1, 2, 255], "nested": [[1, 2], [3]], "number": 1.5, "flag": true,
"text": "hello", "nothing": null, "mixed": [1, 2.5, -3, 4], "flags": [true, false, true]})");
const json j = d.root().materialize();
const json_view v = d.root();
CHECK(same<std::list<int>>(v, j, "ints"));
CHECK(same<std::deque<int>>(v, j, "ints"));
CHECK(same<std::set<int>>(v, j, "ints"));
CHECK(same<std::multiset<int>>(v, j, "ints"));
CHECK(same<std::unordered_set<std::string>>(v, j, "strings"));
CHECK(same<std::forward_list<int>>(v, j, "ints"));
CHECK(same<std::array<int, 4>>(v, j, "ints"));
CHECK(same<std::array<double, 4>>(v, j, "mixed"));
CHECK((v["pairs"][0].get<std::pair<int, std::string>>() == j["pairs"][0].get<std::pair<int, std::string>>()));
CHECK(same<std::vector<std::pair<int, std::string>>>(v, j, "pairs"));
CHECK(same<std::tuple<int, bool, point>>(v, j, "triple"));
CHECK(same<std::map<int, std::string>>(v, j, "pairs"));
CHECK(same<std::unordered_map<int, std::string>>(v, j, "pairs"));
CHECK(same<std::map<std::string, int>>(v, j, "object"));
CHECK(same<std::unordered_map<std::string, int>>(v, j, "object"));
CHECK(same<std::map<color, int>>(v, j, "colors"));
CHECK(same<std::vector<point>>(v, j, "points"));
CHECK(same<std::vector<int>>(v, j, "ints"));
CHECK(same<std::vector<bool>>(v, j, "flags"));
CHECK(same<std::vector<std::uint8_t>>(v, j, "bytes"));
CHECK(same<std::vector<std::vector<int>>>(v, j, "nested"));
CHECK(same<std::vector<json>>(v, j, "mixed"));
CHECK(same<double>(v, j, "number"));
CHECK(same<bool>(v, j, "flag"));
CHECK(same<std::string>(v, j, "text"));
CHECK(same<std::nullptr_t>(v, j, "nothing"));
CHECK(same<json>(v, j, "object"));
const std::valarray<int> va = v["ints"].get<std::valarray<int>>();
CHECK(va.size() == 4);
CHECK(va[0] == 3);
int c_array[2][2] = {{0, 0}, {0, 0}}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays): the conversion to C arrays
v["nested"][0].get_to(c_array[0]);
CHECK(c_array[0][1] == 2);
#ifdef JSON_HAS_CPP_17
CHECK(v["points"][0].get<std::optional<point>>()->y == 2);
CHECK(!v["nothing"].get<std::optional<point>>().has_value());
#endif
// containers of views keep views of the elements
const std::vector<json_view> views = v["points"].get<std::vector<json_view>>();
CHECK(views.size() == 2);
CHECK(views[1]["x"].get<int>() == 3);
CHECK(v["object"].get<std::map<std::string, json_view>>().at("a").get<int>() == 2);
}
SECTION("std types: duplicate keys keep the last value, as from the materialized value")
{
const json_document d = json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
CHECK((d.root().get<std::map<std::string, int>>() == d.root().materialize().get<std::map<std::string, int>>()));
CHECK(d.root().get<std::map<std::string, int>>().at("a") == 3);
CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3);
}
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS
SECTION("std types: the same errors as from the materialized value")
{
const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])");
const json j = d.root().materialize();
for (std::size_t i = 0; i < 6; ++i)
{
CAPTURE(i)
const json_view v = d.root()[i];
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::list<int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::list<int>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::forward_list<int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::forward_list<int>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::array<int, 2>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::array<int, 2>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::pair<int, std::string>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::pair<int, std::string>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::map<std::string, int>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::map<int, int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::map<int, int>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::vector<std::uint8_t>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::vector<std::uint8_t>>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<json::binary_t>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<json::binary_t>()); }));
CHECK(exception_of_call([&] { static_cast<void>(v.get<std::valarray<int>>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<std::valarray<int>>()); }));
}
}
#endif
SECTION("macro types")
{
const json_document d = json_document::parse(R"({"name": "s", "points": [{"x": 1, "y": 2}, {"x": 3, "y": 4}],
"center": {"x": 5, "y": 6}, "named": {"a": {"x": 7, "y": 8}}})");
const shape s = d.root().get<shape>();
CHECK(s.name == "s");
CHECK(s.points.size() == 2);
CHECK(s.points[1].y == 4);
CHECK(s.center.x == 5);
CHECK(s.named.at("a").y == 8);
point p;
d.root()["center"].get_to(p);
CHECK(p.x == 5);
CHECK(p.y == 6);
static_cast<void>(d.root()["center"].get<empty>());
}
SECTION("defaults and names")
{
const json_document d = json_document::parse(R"({"a": 5, "Value": 3, "Text": "t"})");
const with_defaults w = d.root().get<with_defaults>();
CHECK(w.a == 5);
CHECK(w.b == "default");
CHECK(w.c == std::vector<int> {1, 2});
const json_document null_document = json_document::parse("null");
CHECK(null_document.root().get<with_defaults>().a == 1);
const with_names n = d.root().get<with_names>();
CHECK(n.value == 3);
CHECK(n.text == "t");
}
SECTION("enums")
{
const json_document d = json_document::parse(R"(["green", null, "purple", 1, "red"])");
CHECK(d.root()[0].get<color>() == color::green);
CHECK(d.root()[1].get<color>() == color::blue);
CHECK(d.root()[2].get<color>() == color::red); // unknown values map to the first entry
CHECK(d.root()[3].get<strict_color>() == strict_color::green);
CHECK(d.root()[4].get<strict_color>() == strict_color::red);
#if !defined(JSON_NOEXCEPTION)
const json j = d.root().materialize();
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
#if !JSON_DIAGNOSTICS
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
#endif
#endif
}
SECTION("derived types")
{
const json_document d = json_document::parse(R"({"id": 7, "c": "green", "extra": 3})");
const derived x = d.root().get<derived>();
CHECK(x.id == 7);
CHECK(x.c == color::green);
const derived_with_defaults y = d.root().get<derived_with_defaults>();
CHECK(y.id == 7);
CHECK(y.label == "none");
const derived_from_legacy z = d.root().get<derived_from_legacy>();
CHECK(z.id == 7);
CHECK(z.extra == 3);
}
SECTION("other from_json() functions get the value of materialize()")
{
const json_document d = json_document::parse(R"({"v": 4})");
CHECK(d.root().get<legacy>().v == 4);
CHECK(d.root().get<unconstrained>().v == 4);
CHECK(d.root().get<constrained>().v == 4);
legacy l;
d.root().get_to(l);
CHECK(l.v == 4);
}
SECTION("duplicate keys: members are looked up in the view, where the first one wins")
{
const json_document d = json_document::parse(R"({"x": 1, "y": 2, "x": 3})");
CHECK(d.root().get<point>().x == 1);
// materialize() keeps the last value, like parse()
CHECK(d.root().materialize().get<point>().x == 3);
}
SECTION("editable documents and ordered_json")
{
json_editable_document e = json_editable_document::parse(R"({"x": 1, "y": 2})");
e.set(e.root(), "x", 10);
CHECK(e.root().get<point>().x == 10);
const ordered_json_document o = ordered_json_document::parse(R"({"name": "o", "points": [], "center": {"x": 1, "y": 2}, "named": {}})");
CHECK(o.root().get<shape>().center.y == 2);
}
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS
SECTION("errors are those of the same conversion of a json value")
{
// (with JSON_DIAGNOSTICS, those of json name the path of the value)
const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])");
const json j = d.root().materialize();
for (std::size_t i = 0; i < 3; ++i)
{
CAPTURE(i)
CHECK(exception_of_call([&] { static_cast<void>(d.root()[i].get<point>()); }) == exception_of_call([&] { static_cast<void>(j[i].get<point>()); }));
}
CHECK(exception_of_call([&] { static_cast<void>(d.root()[3].get<derived>()); }) == exception_of_call([&] { static_cast<void>(j[3].get<derived>()); }));
}
#endif
}