Header-only, reflection-based JSON serializer for C++17.
Automatically serializes and deserializes any C++ object to and from JSON with a single macro — no boilerplate to_json/from_json per field.
#include "JsonReflect.hpp"
enum class Difficulty { Easy, Medium, Hard };
struct GameSettings {
int volume = 50;
float sensitivity = 1.0f;
bool fullscreen = true;
Difficulty difficulty = Difficulty::Medium;
};
JSON_REFLECT(GameSettings, volume, sensitivity, fullscreen, difficulty);
GameSettings input{};
JsonReflect::json j = JsonReflect::to_json(input);Output:
{
"volume": 50,
"sensitivity": 1.0,
"fullscreen": true,
"difficulty": "Medium"
}1. Install — grab the single header, then:
#include "JsonReflect.hpp"2. Reflect your struct/class
struct GameSettings {
int volume = 50;
float sensitivity = 1.0f;
bool fullscreen = true;
Difficulty difficulty = Difficulty::Medium;
};
JSON_REFLECT(GameSettings, volume, sensitivity, fullscreen, difficulty);3. Serialize
GameSettings input;
input.volume = 75;
input.sensitivity = 1.5f;
input.fullscreen = false;
input.difficulty = Difficulty::Hard;
JsonReflect::json serialized = JsonReflect::to_json(input);
std::ofstream("settings.json") << serialized.dump();4. Deserialize
JsonReflect::json parsed = JsonReflect::json::parse(std::ifstream("settings.json"));
GameSettings output{};
JsonReflect::from_json(parsed, output);
assert(output.volume == 75);
assert(output.sensitivity == 1.5f);
assert(output.fullscreen == false);
assert(output.difficulty == Difficulty::Hard);JsonReflect requires C++17 or later.
Download single_header/JsonReflect.hpp. It bundles the library and all of its dependencies into one file.
#include "JsonReflect.hpp"Download every header from the repository root plus the extern/ folder, keeping them side by side:
JsonReflect.hpp // umbrella header — include this
JsonReflect_defines.hpp // configuration + dependency include paths
JsonReflect_macro.hpp // JSON_REFLECT macros + tags (lightweight, no heavy deps)
JsonReflect_helpers.hpp
JsonReflect_entry.hpp // to_json / from_json / get_changes
JsonReflect_primitives.hpp // enums + nlohmann fallbacks
JsonReflect_stl.hpp // containers, smart pointers, variant, ...
extern/ // nlohmann/json, visit_struct, magic_enum, svh/tag_invoke
#include "JsonReflect.hpp"Tip
Annotate types without the full include. JSON_REFLECT(...) and BEFRIEND_JSON_REFLECT() live in the lightweight JsonReflect_macro.hpp, which does not pull in nlohmann/json. Include just that header where you declare your types, and include the full JsonReflect.hpp only in the translation units that actually serialize:
// my_struct.hpp — cheap to include everywhere
#include "JsonReflect_macro.hpp"
namespace MyProject {
struct MyStruct { int a = 0; float b = 0.0f; double c = 0.0; };
}
JSON_REFLECT(MyProject::MyStruct, a, b, c);Out of the box, JsonReflect handles:
| Category | Types |
|---|---|
| Booleans & numbers | bool, all char types, all integer types, float, double, long double |
| Strings | std::string (and other string-like types) |
| Enums | any enum / enum class — serialized by name via magic_enum |
| Sequence containers | std::vector, std::array, std::list, std::forward_list, std::deque |
| Sets | std::set, std::multiset, std::unordered_set, std::unordered_multiset |
| Maps | std::map, std::multimap, std::unordered_map, std::unordered_multimap (as JSON objects) |
| Tuple-likes | std::pair, std::tuple, std::optional |
| Sum types | std::variant (stored as { "index": N, "value": ... }) |
| Pointers | raw pointers, std::unique_ptr, std::shared_ptr, std::weak_ptr (serialize only) |
| Passthrough | nlohmann::json values are stored/loaded as-is |
| Your types | any struct/class marked with JSON_REFLECT, including nested and containers of them |
Enums serialize to a readable string and error on an unknown name:
{ "difficulty": "Hard" }JSON_REFLECT(Type, field1, field2, ...) registers a type for serialization, deserialization, and diffing in one line. Place it at namespace scope (outside any namespace), naming the type fully qualified:
namespace ns {
struct GameSettings { int volume = 50; float sensitivity = 1.0f; };
}
JSON_REFLECT(ns::GameSettings, volume, sensitivity);Add BEFRIEND_JSON_REFLECT() inside a class to let JsonReflect read and write private fields:
struct PrivateSettings {
private:
int volume = 50;
float sensitivity = 1.0f;
BEFRIEND_JSON_REFLECT()
};
JSON_REFLECT(PrivateSettings, volume, sensitivity);JSON_REFLECT is shorthand for three independent registrations. Use them separately when you want serialize, deserialize, and compare to cover different fields:
struct Foo { int a = 50; float b = 1.0f; bool c = true; Difficulty d = Difficulty::Medium; };
JSON_REFLECT_SERIALIZE(Foo, a, b, c); // write a, b, c
JSON_REFLECT_DESERIALIZE(Foo, a, b); // read only a, b
JSON_REFLECT_COMPARE(Foo, a); // diff only aFields left out of the deserialize list keep their default/constructed values when loading.
Reflect a class template with JSON_REFLECT_TEMPLATE. Wrap the template parameters and arguments in parentheses:
template <typename T>
struct Boxed {
T value{};
int tag = 0;
};
JSON_REFLECT_TEMPLATE((typename T), Boxed, (T), value, tag);JSON_REFLECT_SERIALIZE_TEMPLATE, JSON_REFLECT_DESERIALIZE_TEMPLATE, and JSON_REFLECT_COMPARE_TEMPLATE are available for partial template reflection.
When reflection isn't enough, implement a tag_invoke overload. JsonReflect finds it automatically (see Resolution Order).
struct CustomObject {
int id = 0;
std::string name;
float value = 0.0f;
void init();
};
inline JsonReflect::json tag_invoke(JsonReflect::serialize_t, const CustomObject& o) {
JsonReflect::json j;
j["id"] = JsonReflect::to_json(o.id);
j["name"] = JsonReflect::to_json(o.name);
j["value"] = JsonReflect::to_json(o.value);
return j;
}
inline void tag_invoke(JsonReflect::deserialize_t, const JsonReflect::json& j, CustomObject& o) {
if (j.contains("id")) JsonReflect::from_json(j["id"], o.id);
if (j.contains("name")) JsonReflect::from_json(j["name"], o.name);
if (j.contains("value")) JsonReflect::from_json(j["value"], o.value);
o.init(); // run custom logic after loading
}Keep the generated reflection but hook in extra logic — for example, calling init() after deserialization:
JSON_REFLECT(CustomObject, id, name, value);
inline void tag_invoke(JsonReflect::deserialize_t, const JsonReflect::json& j, CustomObject& o) {
JsonReflect::Detail::from_json_visitable(j, o); // default reflected deserialization
o.init();
}Both to_json and from_json forward any extra arguments through to your tag_invoke overloads and into the elements of containers. This lets you thread context (an allocator, a version number, a flag) through a whole object graph:
JsonReflect::json tag_invoke(JsonReflect::serialize_t, const MyStruct& v, bool verbose) { /* ... */ }
auto j = JsonReflect::to_json(my_struct, /*verbose=*/true);
// forwarded to every element as well:
std::vector<MyStruct> items = /* ... */;
auto arr = JsonReflect::to_json(items, true);When (de)serializing a type, JsonReflect picks an implementation at compile time in this order:
- Your
tag_invoke—serialize_t/deserialize_t/compare_toverloads you defined. - Reflection — types registered with
JSON_REFLECT. - Library
tag_invoke— JsonReflect's built-in support (enums, containers, smart pointers, variant, ...). - nlohmann::json defaults — anything nlohmann already knows how to handle.
Because your overloads come first, you can always override library or reflection behavior. If nothing matches, compilation fails with:
JsonReflect Error: No suitable serialize implementation found for type T
Raw pointers are dereferenced automatically. A null pointer serializes to null. When deserializing, the pointer must already point at valid storage:
int value = 0;
int* p = &value;
JsonReflect::from_json(j, p); // writes into *p — allocate before callingstd::unique_ptr and std::shared_ptr serialize to their pointee (or null). On deserialize, a null JSON resets the pointer; otherwise a null smart pointer is auto-initialized (see JSON_REFLECT_INITIALIZE_SMART_POINTERS) before loading into it.
std::weak_ptr serializes the locked value (or null). Deserialization of weak_ptr is intentionally not supported and will fail to compile.
For non-default-constructible pointee types, specialize the factory used to auto-initialize shared_ptr:
template<>
struct JsonReflect::Detail::smart_pointer_factory<CustomObject> {
static std::shared_ptr<CustomObject> create() {
return std::make_shared<CustomObject>(/* your args */);
}
};Delta serialization writes only the fields that differ from a baseline, producing smaller JSON. It is opt-in per type:
struct MyStruct { int a = 42; float b = 3.14f; bool c = true; std::string d = "Hello"; };
JSON_REFLECT(MyStruct, a, b, c, d);
// enable delta serialization for this type
template<>
struct JsonReflect::Detail::delta_serialize<MyStruct> : std::true_type {};
MyStruct input{};
input.a = 100;
input.b = 2.71f; // c and d unchanged
JsonReflect::json j = JsonReflect::to_json(input);Result (unchanged c and d are omitted):
{
"a": 100,
"b": 2.71
}By default the baseline is a default-constructed instance, so delta serialization requires a default-constructible type. For types that aren't, specialize delta_default to supply the baseline:
struct NonDefaultConstructible {
int a; float b; bool c;
NonDefaultConstructible(int a, float b, bool c) : a(a), b(b), c(c) {}
};
JSON_REFLECT(NonDefaultConstructible, a, b, c);
template<>
struct JsonReflect::Detail::delta_serialize<NonDefaultConstructible> : std::true_type {};
template<>
struct JsonReflect::Detail::delta_default<NonDefaultConstructible> {
static NonDefaultConstructible make() {
return NonDefaultConstructible{ 0, 0.0f, true };
}
};Important
Change detection uses each field's == / != operator. If a field type has neither, JsonReflect falls back to serializing both values and comparing the resulting JSON — correct, but more expensive. That fallback can be disabled with JSON_REFLECT_ALLOW_JSON_COMPARE.
get_changes(lhs, rhs) compares two objects of the same type and returns a JSON object containing only the fields whose value in rhs differs from lhs (the rhs value is what gets stored). It recurses into nested reflected types, emitting just the changed leaves.
GameSettings a{}; // defaults
GameSettings b{};
b.volume = 75;
b.fullscreen = false;
JsonReflect::json diff = JsonReflect::get_changes(a, b);Result:
{
"volume": 75,
"fullscreen": false
}It works for types registered with JSON_REFLECT (which registers the compare context) and for types with an == operator, and recurses through nested reflected members. Register a custom tag_invoke(JsonReflect::compare_t, const T&, const T&) to override the diff for a specific type.
Note
get_changes vs. delta serialization. Delta serialization compares an object against a fixed baseline (its default, or your delta_default) during to_json. get_changes compares two explicit objects you provide. Use delta serialization for "save only what changed from defaults"; use get_changes for "what changed between these two snapshots".
Define any of these before including JsonReflect to change its behavior. All have safe defaults.
| Macro | Default | Effect |
|---|---|---|
JSON_REFLECT_STATIC_FOR_DELTA |
1 |
Reuse a static baseline instance during delta serialization (fewer allocations, but the baseline stays alive). |
JSON_REFLECT_ALLOW_JSON_COMPARE |
1 |
Allow the JSON-compare fallback for delta detection on fields with no ==/!=. If 0, such fields cause a compile error. |
JSON_REFLECT_INITIALIZE_SMART_POINTERS |
1 |
Auto-initialize a null shared_ptr/unique_ptr before deserializing into it. If 0, a null smart pointer errors instead. |
JSON_REFLECT_ALLOW_THROW |
1 |
Throw on runtime errors (null pointers, invalid enum names, ...). If 0, JsonReflect logs to stderr and continues where possible. |
JSON_USE_IMPLICIT_CONVERSIONS |
0 |
Passed through to nlohmann/json. Disabled by default to avoid accidental implicit conversions to JSON. |
Example:
#define JSON_REFLECT_ALLOW_THROW 0
#include "JsonReflect.hpp"By default JsonReflect includes its dependencies from their conventional paths (<nlohmann/json.hpp>, etc.). If your project vendors these under different names or folders, redirect them by defining the matching macro before including JsonReflect:
| Macro | Default |
|---|---|
JSON_REFLECT_NLOHMANN_JSON_HPP |
<nlohmann/json.hpp> |
JSON_REFLECT_VISIT_STRUCT_HPP |
<visit_struct/visit_struct.hpp> |
JSON_REFLECT_SVH_TAG_INVOKE_HPP |
<svh/tag_invoke.hpp> |
JSON_REFLECT_MAGIC_ENUM_HPP |
<magic_enum/magic_enum.hpp> |
#define JSON_REFLECT_NLOHMANN_JSON_HPP <my/vendored/json.hpp>
#define JSON_REFLECT_MAGIC_ENUM_HPP "third_party/magic_enum.hpp"
#include "JsonReflect.hpp"Note
This applies to the separate-header build. The single-header build already has every dependency inlined, so these macros aren't needed there.
The single header is generated from the individual headers by a script. Regenerate it after changing any library header:
python scripts/generate_single_header.py
# or write somewhere else:
python scripts/generate_single_header.py path/to/JsonReflect.hppThe script inlines the extern/ dependencies and all JsonReflect_* headers in dependency order into single_header/JsonReflect.hpp.
All dependencies are bundled in the single header and mirrored in extern/:
- nlohmann::json — JSON parsing and serialization
- visit_struct — compile-time struct reflection
- magic_enum — enum reflection
- svh/tag_invoke — the
tag_invokedispatch mechanism - C++17 or later
Feedback, issues, and pull requests are welcome! This project is part of my university work, so everything is a learning experience.
MIT License — see LICENSE.
As a student, credit or a quick note about what you're using it for is greatly appreciated. It motivates me to keep contributing to open source!