https://github.com/byebyebryan/lazy-serializable
The laziest way to add serialization to C++ data classes.
https://github.com/byebyebryan/lazy-serializable
binary cmake cpp cpp20 data-serialization header-only json prototyping serialization single-header toml yaml
Last synced: 18 days ago
JSON representation
The laziest way to add serialization to C++ data classes.
- Host: GitHub
- URL: https://github.com/byebyebryan/lazy-serializable
- Owner: byebyebryan
- License: mit
- Created: 2025-11-26T18:22:26.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2026-04-11T20:06:52.000Z (about 1 month ago)
- Last Synced: 2026-04-11T20:11:01.391Z (about 1 month ago)
- Topics: binary, cmake, cpp, cpp20, data-serialization, header-only, json, prototyping, serialization, single-header, toml, yaml
- Language: C++
- Size: 63.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# lazy-serializable
[](https://github.com/byebyebryan/lazy-serializable/actions/workflows/ci.yml)
[](LICENSE)
[](https://isocpp.org/)
[]()
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 `.proto` or 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.
### What it doesn't do
- **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).
## Quick start (single header)
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.
```cpp
#include "lazy_serializable.h"
// Define your data with one macro per field
struct SensorConfig : lazy::JsonSerializable {
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 objects and sealed types
Nested types work automatically as long as they inherit from `lazy::Serializable`. For external/“sealed” types, use `LAZY_SERIALIZABLE_TYPE`.
```cpp
// Standard lazy-serializable struct
struct Address : lazy::JsonSerializable
{
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);
}
```
## MultiSerializable (optional)
`lazy::MultiSerializable` registers fields for multiple adapters at once, allowing you to switch formats on the fly.
```cpp
#include "lazy_serializable.h"
// Register for ALL enabled adapters
struct Record : lazy::MultiSerializable {
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(std::cout);
// Output: {"id":"A1","value":42}
// Serialize to Binary
rec.serialize(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.
## Installation and CMake integration
### Single-header distribution
- Copy `include/lazy_serializable.h` into 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.
### As a subproject (add_subdirectory / FetchContent)
```cmake
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`).
### Installed package (find_package)
After installing the project (headers and CMake package config), you can consume it via:
```cmake
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 overview
| 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` |
### Adapter limitations
- **Lazy JSON**: minimal JSON implementation, trades completeness for small size; precision is limited by `std::stod` and strings with `\uXXXX` escapes 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:
```cpp
#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=1
```
If 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.
## Feature toggles
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.
## Development workflow
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.
### Generating the single header
- 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`
### Running the tests
`build.sh` and the provided `CMakeLists.txt` are intended for development. They:
1. Enable all optional adapters by default (and fetch dependencies through `FetchContent`).
2. Build from the modular headers.
3. Provide an opt-in mode to run the test suite against the generated single header as well (`./build.sh --single-header-tests` or `-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.
## Custom adapters
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).
```cpp
class CsvAdapter {
public:
static CsvAdapter fromStream(std::istream& is);
void toStream(std::ostream& os) const;
template
void writeField(const char* name, const T& value);
template
void readField(const char* name, T& out);
};
struct CsvRow : lazy::Serializable {
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.
## Best practices
- Always specify meaningful default values in `LAZY_SERIALIZABLE_FIELD` to 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::serializable` to 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.
## License
lazy-serializable is available under the MIT License. See [LICENSE](LICENSE) for details.