An open API service indexing awesome lists of open source software.

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.

Awesome Lists containing this project

README

          

# lazy-serializable

[![CI](https://github.com/byebyebryan/lazy-serializable/actions/workflows/ci.yml/badge.svg)](https://github.com/byebyebryan/lazy-serializable/actions/workflows/ci.yml)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![C++20](https://img.shields.io/badge/C%2B%2B-20-blue.svg)](https://isocpp.org/)
[![Header Only](https://img.shields.io/badge/header--only-yes-green.svg)]()

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.