Provides the laziest way to add serialization to C++ data classes.
Adding serialization to a project often introduces unwanted friction: learning a complex IDL, setting up code generation steps, or maintaining repetitive load/save boilerplate.
lazy-serializable solves this by letting you declare fields once, inline, using a simple macro. It effectively "glues" your data structures to a wide range of formats—JSON, Binary, Text, YAML, TOML—without requiring you to change your data model or build process.
Designed for rapid prototyping and simple projects, it offers:
- Zero Friction: Header-only, no external dependencies (for built-in adapters), no build steps.
- Code-First: No
.protoor schema files; your C++ struct is the source of truth. - Multi-Format: Switch between human-readable JSON/Text (for debugging) and compact Binary (for release) instantly.
- Composition: Automatically handles nested objects,
std::vector, and sealed third-party types. - Extensible: Don't see the format you need? Writing a custom adapter is trivial (often ~50 lines) and works instantly with all your existing data types.
- No Pointer Chasing: Focuses on value types and composition; does not handle pointers, references, or object graphs with cycles.
- No Schema Validation: Assumes data fits the structure; validation is left to the underlying backend or user logic.
- No Built-in Versioning: You control your data evolution (e.g., by adding version fields).
The recommended distribution is the amalgamated header include/lazy_serializable.h. Drop that file into your project (or include it via your package manager) and pick the adapter you need.
#include "lazy_serializable.h"
// Define your data with one macro per field
struct SensorConfig : lazy::JsonSerializable<SensorConfig> {
LAZY_SERIALIZABLE_FIELD(std::string, name, "default");
LAZY_SERIALIZABLE_FIELD(int, sample_rate_hz, 1);
LAZY_SERIALIZABLE_FIELD(bool, enabled, true);
};
int main() {
SensorConfig cfg;
cfg.name = "temp";
// Serialize to JSON
std::cout << cfg;
// Output: {"name":"temp","sample_rate_hz":1,"enabled":true}
// Deserialize from JSON
std::stringstream input(R"({"name":"new","sample_rate_hz":10,"enabled":false})");
input >> cfg;
}Nested types work automatically as long as they inherit from lazy::Serializable. For external/“sealed” types, use LAZY_SERIALIZABLE_TYPE.
// Standard lazy-serializable struct
struct Address : lazy::JsonSerializable<Address> {
LAZY_SERIALIZABLE_FIELD(std::string, city, "");
};
// External struct (e.g. from a 3rd party lib)
struct User {
std::string name;
Address home;
};
// Register the external type non-intrusively
namespace lazy::serializable {
LAZY_SERIALIZABLE_TYPE(JsonAdapter, User, name, home);
}lazy::MultiSerializable registers fields for multiple adapters at once, allowing you to switch formats on the fly.
#include "lazy_serializable.h"
// Register for ALL enabled adapters
struct Record : lazy::MultiSerializable<Record> {
LAZY_MULTI_SERIALIZABLE_FIELD(std::string, id, "");
LAZY_MULTI_SERIALIZABLE_FIELD(int, value, 0);
};
Record rec;
rec.id = "A1";
rec.value = 42;
// Serialize to JSON
rec.serialize<lazy::serializable::JsonAdapter>(std::cout);
// Output: {"id":"A1","value":42}
// Serialize to Binary
rec.serialize<lazy::serializable::BinaryAdapter>(std::cout);
// Output: [compact binary data]To control which adapters MultiSerializable uses, define LAZY_MULTI_SERIALIZABLE_ADAPTERS before including the header, or override the relevant LAZY_SERIALIZABLE_ENABLE_* toggles to restrict which adapters are compiled in.
- Copy
include/lazy_serializable.hinto your project or install the package from your package manager. - Configure feature toggles either via preprocessor defines before the include or via CMake options if you use the project as a subdirectory.
add_subdirectory(lazy-serializable)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE lazy::serializable)All optional adapters that are enabled at configure time are available automatically; link their interface targets if you need them explicitly (e.g. lazy::serializable::rapid-json).
After installing the project (headers and CMake package config), you can consume it via:
find_package(lazy-serializable CONFIG REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE lazy::serializable)When using the project as a subdirectory, adapter-specific interface targets such as
lazy::serializable::rapid-json, lazy::serializable::nlohmann-json,
lazy::serializable::yaml, and lazy::serializable::toml are also available when those
adapters are enabled at configure time.
| Adapter | Header | Macro | Dependencies | CMake target |
|---|---|---|---|---|
| Lazy JSON (default) | lazy/adapters/json_lazy.h |
LAZY_SERIALIZABLE_ENABLE_LAZY_JSON (default 1) |
none | lazy::serializable |
| RapidJSON | lazy/adapters/json_rapid.h |
LAZY_SERIALIZABLE_ENABLE_RAPID_JSON |
RapidJSON | lazy::serializable::rapid-json |
| nlohmann/json | lazy/adapters/json_nlohmann.h |
LAZY_SERIALIZABLE_ENABLE_NLOHMANN_JSON |
nlohmann/json | lazy::serializable::nlohmann-json |
| Binary | lazy/adapters/binary.h |
LAZY_SERIALIZABLE_ENABLE_BINARY (default 1) |
none | lazy::serializable |
| Text (key/value) | lazy/adapters/text.h |
LAZY_SERIALIZABLE_ENABLE_TEXT (default 1) |
none | lazy::serializable |
| YAML | lazy/adapters/yaml.h |
LAZY_SERIALIZABLE_ENABLE_YAML |
fkYAML | lazy::serializable::yaml |
| TOML | lazy/adapters/toml.h |
LAZY_SERIALIZABLE_ENABLE_TOML |
toml++ | lazy::serializable::toml |
- Lazy JSON: minimal JSON implementation, trades completeness for small size; precision is limited by
std::stodand strings with\uXXXXescapes are round-tripped as?in those positions. - Binary: compact, order-dependent format; uses host endianness (only portable across machines with the same endianness).
- Text: human-readable key/value format; does not support arrays of objects or sealed types that contain arrays.
- YAML/TOML/RapidJSON/nlohmann/json: rely on the underlying libraries for exact syntax/semantics; lazy-serializable adds only a thin mapping layer.
Choose the JSON backend by defining one of:
#define LAZY_SERIALIZABLE_JSON_BACKEND_RAPIDJSON // requires LAZY_SERIALIZABLE_ENABLE_RAPID_JSON=1
// or
#define LAZY_SERIALIZABLE_JSON_BACKEND_NLOHMANN_JSON // requires LAZY_SERIALIZABLE_ENABLE_NLOHMANN_JSON=1If neither is defined, the lightweight builtin LazyJsonAdapter is used.
When using the single header, set the corresponding LAZY_SERIALIZABLE_ENABLE_* macro to 1 before including it so the adapter code is available.
All major components can be enabled/disabled via preprocessor defines before including the headers or via CMake options with the same names.
| Macro | Default (single header) | Purpose |
|---|---|---|
LAZY_SERIALIZABLE_ENABLE_MULTI |
1 |
Provide lazy::MultiSerializable |
LAZY_SERIALIZABLE_ENABLE_LAZY_JSON |
1 |
Include LazyJsonAdapter |
LAZY_SERIALIZABLE_ENABLE_BINARY |
1 |
Include BinaryAdapter |
LAZY_SERIALIZABLE_ENABLE_TEXT |
1 |
Include TextAdapter |
LAZY_SERIALIZABLE_ENABLE_RAPID_JSON |
0 (dev CMake defaults to ON) |
Include RapidJSON adapter |
LAZY_SERIALIZABLE_ENABLE_NLOHMANN_JSON |
0 (dev CMake defaults to ON) |
Include nlohmann/json adapter |
LAZY_SERIALIZABLE_ENABLE_YAML |
0 (dev CMake defaults to ON) |
Include YAML adapter |
LAZY_SERIALIZABLE_ENABLE_TOML |
0 (dev CMake defaults to ON) |
Include TOML adapter |
Define any of them to 0 to trim unused code from the single header. Define to 1 (and provide the required dependency) to enable optional adapters.
The modular headers under src/lazy/... are the canonical source for contributors. The amalgamated lazy_serializable.h is generated from them and should not be edited manually.
See AGENTS.md for contributor workflow, conventions, and project structure.
- Manually:
python3 scripts/generate_single_header.py - Via CMake target:
cmake --build build --target lazy-serializable-single-header - Via the helper script:
./build.sh --regen-single-header
build.sh and the provided CMakeLists.txt are intended for development. They:
- Enable all optional adapters by default (and fetch dependencies through
FetchContent). - Build from the modular headers.
- Provide an opt-in mode to run the test suite against the generated single header as well (
./build.sh --single-header-testsor-DLAZY_SERIALIZABLE_TEST_SINGLE_HEADER=ON).
Use these scripts when contributing; for packaging, use the generated single header (or copy the few modular headers you need) and configure the feature toggles to match your environment.
One of the core design goals is extensibility. If you need a format not supported out of the box (e.g., MessagePack, XML, or a custom protocol), you don't need to fork the library.
Just implement a class with four methods (fromStream, toStream, writeField, readField), and it will immediately work with all lazy::Serializable features (nested types, arrays, sealed types).
class CsvAdapter {
public:
static CsvAdapter fromStream(std::istream& is);
void toStream(std::ostream& os) const;
template <typename T>
void writeField(const char* name, const T& value);
template <typename T>
void readField(const char* name, T& out);
};
struct CsvRow : lazy::Serializable<CsvRow, CsvAdapter> {
LAZY_SERIALIZABLE_FIELD(std::string, symbol, "");
LAZY_SERIALIZABLE_FIELD(double, price, 0.0);
};Once your adapter implements writeField/readField and fromStream/toStream, all Serializable/MultiSerializable features (nested types, sealed types, vectors, etc.) work automatically.
- Always specify meaningful default values in
LAZY_SERIALIZABLE_FIELDto keep backward compatibility when new fields are added. - Include versioning fields for on-disk formats to detect schema changes early.
- When using
MultiSerializable, keep the adapter list minimal to avoid unnecessary code size or dependency pulls. - For sealed/external types, prefer registering adapters near the type definition inside
namespace lazy::serializableto ensure ADL finds the helpers. - BinaryAdapter uses the host’s endianness; if you need cross-endian portability, add an explicit byte-order conversion step on read/write.
lazy-serializable is available under the MIT License. See LICENSE for details.