alpaca: packing C++ structs into bytes without macros or code generation
Serialization library written in C++17 - Pack C++ structs into a compact byte-array without any macros or boilerplate code
At a glance
- What is it?
- alpaca is a header-only C++17 serialization library that works by reflecting over a struct rather than by asking you to annotate it, so there are no macros, no boilerplate and no code generation step. It packs into byte arrays, vectors or files, handles STL containers, optionals, variants and recursive structures, and adds optional compile-time features for type hashing, schema versioning and checksum verification.
- Who is it for?
- Adopt alpaca if you are writing C++17 and want serialization without macros, without a code generation step and without extra dependencies, and if reflection is acceptable in your codebase, since the entire design depends on it.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 20 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The pitch: no macros, no boilerplate, no code generation
alpaca packs C++ structs into a compact byte-array without any macros or boilerplate code, and the first bullet of the feature list says the same thing in library form: it is a header-only serialization library for modern C++, written in C++17, with no macros or boilerplate, no source code generation and no external dependencies.
The distinction is worth being precise about, because serialization libraries divide into two camps. The first camp asks you to declare what you want serialized, usually with a macro above each field or a registration call, and the library walks those declarations. The second camp reflects over the type at compile time and works out the layout itself.
alpaca is in the second camp, and that is why the header-only and no-code-generation claims belong together. There is nothing to generate, so there is no generator to run, no build step to wire in, and no generated file to keep in sync with your headers. You add a header, include it, and call a function.
The example in the README shows the shape of it. A Config struct holds a std::string, a std::pair, a std::array of nine doubles, a std::vector of floats and a std::map of string to variant. The object is constructed, serialized into a std::vector<uint8_t> with alpaca::serialize, and deserialized with alpaca::deserialize<Config> taking an error code out parameter:
#include <alpaca/alpaca.h>
struct Config {
std::string device;
std::pair<unsigned, unsigned> resolution;
std::array<double, 9> K_matrix;
std::vector<float> distortion_coeffients;
std::map<std::string, std::variant<uint16_t, std::string, bool>> parameters;
};
// Construct the object
Config c{"/dev/video0", {640, 480},
{223.28249888247538, 0.0, 152.30570853111396,
0.0, 223.8756535707556, 124.5606000035353,
0.0, 0.0, 1.0},
{-0.44158343539568284, 0.23861463831967872, 0.0016338407443826572,
0.0034950038632981604, -0.05239245892096022},
{{"start_server", bool{true}},
{"max_depth", uint16_t{5}},
{"model_path", std::string{"foo/bar.pt"}}}};
// Serialize
std::vector<uint8_t> bytes;
auto bytes_written = alpaca::serialize(c, bytes);
// Deserialize
std::error_code ec;
auto object = alpaca::deserialize<Config>(bytes, ec);
if (!ec) {
// use object
}Look at the struct definition. There is no macro above it, no field list, no registration. The library works out from the type that there are five members and what each one is. The map with a variant value type is the interesting part of that particular example, since a variant has to be described in the encoded stream in a way the decoder can read back, and it does it without a schema file.
The error_code out parameter is also worth noting. Deserialization reports failure through std::error_code rather than by throwing or by returning a sentinel, which is the convention for a library that has to work in embedded and no-exceptions builds.
The primary language is C++, the licence is MIT, the default branch is master, and the recent releases are v0.3.0 from 2026-10-02, v0.2.1 from 2022-09-17 and v0.2.0 from 2022-09-07. The last push was on 2026-10-05 with the project not archived.
What the type system has to support
The feature list names the type coverage, and it is broader than the fundamental types.
Supported are basic data types, STL containers, unique pointers, recursive data structures, optionals, variants and more. Output can go to C-style arrays, std::array, std::vector, or even directly to files.
The table of contents shows the per-type examples in detail, and they map onto a set of C++ features that make reflection hard. Fundamental types are the base case. Arrays, vectors and strings cover the contiguous and dynamic array cases. Multi-byte character strings are called out separately, which suggests a distinct encoding path rather than a byte-per-character one. Maps and sets cover the associative containers. Nested structures cover the recursive case where a struct contains another struct. Optional values cover std::optional, where the encoded form has to distinguish present from absent. Variant types are called type-safe unions and cover std::variant, where the encoded form has to record which alternative was written. Smart pointers and recursive data structures cover unique_ptr and a struct that contains a pointer to its own type. Timestamps and durations cover the chrono types.
Two of these are genuinely hard and worth understanding.
A std::variant needs a tag. When you serialize a variant, the decoder has to know which alternative it is holding, so the stream carries a discriminant. And std::optional needs the same thing in reverse, a flag saying whether the value is there at all. Neither is a big design problem but both are the kind of thing that a hand-rolled reflection library tends to get subtly wrong, and getting them right is what lets the same struct be serialized and read back without a schema.
Recursive structures are the third interesting case, because the encoder has to track which types it has already seen in order to detect a cycle, and a naive implementation either recurses forever or needs a depth limit that changes the output format.
One note in the API section explains a wrinkle in the design. Under most circumstances the number of fields in the struct, N, need not be provided, but in certain use cases, for example std::optional, the user will need to provide this N for correct operation. So the template parameter that most users never think about is required in some cases, which is a fair thing to know before you hit it.
Compile-time configuration, and the features that cost something
The library is highly configurable at compile time, and there are four switches worth understanding, because two of them are about correctness rather than about size.
Endianness. Little endian by default, configurable to use big endian byte order. The default matters more than it looks, because little endian is what most of the hardware you will run on uses, and a serialized file that is byte-order dependent is not portable between architectures. Making it a compile-time choice rather than a runtime one means the choice is made once and baked in.
Fixed or variable-length encoding. Variable-length encoding by default for large integer types, configurable to use fixed-width encoding. Variable-length encoding means a small value takes fewer bytes, so a struct of mostly small numbers gets compact. Fixed-width means every integer takes its full width, so the size is predictable and a decoder can skip a field without reading it.
That trade is a classic one and the default tells you what the author thinks. Variable-length by default means compact output is the priority, and fixed-width is there for the case where you need to seek or where the size must be known in advance.
Type hashing and data structure versioning. Optional, and described as recursively generating a type hash that is checked during deserialization. So when you serialize, the library computes a hash of the type's structure, and when you deserialize, it checks the hash of what it is decoding against. If the struct changed since the data was written, the deserialization fails rather than producing garbage.
Integrity checking. Optional, detects data corruption during deserialization using checksums.
Those last two are the ones that make the library more than a packer. Without them, a serialized buffer is only as trustworthy as the process that produced it, and a version mismatch reads as valid nonsense. With them, you get the two failure modes handled properly: the data is not what it claims to be, and the data is not what the current code expects.
There is one more escape hatch in the configuration list. Macros to Exclude STL Data Structures, which lets you opt a particular standard library type out of the automatic handling, so you can provide your own treatment for it.
Aligned memory access is also on the list of configuration options, which is a hint that the library can be told to produce output suitable for direct memory reinterpretation rather than only for a stream.
The options are passed as a template parameter, so the configuration is part of the type:
std::vector<uint8_t> bytes;
constexpr auto OPTIONS = options::fixed_length_encoding |
options::with_version |
options::with_checksum;
auto bytes_written = serialize<OPTIONS>(object, bytes);The three flags in that example combine fixed-length encoding, versioning and checksums, and they are combined with a bitwise or into a constexpr value that is then passed as the template argument to serialize. Because it is a template parameter, the choices are resolved at compile time rather than checked at runtime.
The API shape, and where the boilerplate really goes
The API is two functions and a set of options, and it is worth looking at the signatures because they explain the design.
// Serialize a struct T (with N fields) into Container
template <class T, size_t N, class Container>
auto serialize(const T&, Container&) -> size_t /* bytes_written */;
// Serialize a struct T (with N fields) into Container using options O
template <options O, class T, size_t N, class Container>
auto serialize(const T&, Container&) -> size_t /* bytes_written */;So serialize takes a const reference to an aggregate class type, typically a struct, and an output container, and returns the number of bytes written. The variant with an options template parameter is the second form. The container can be a std::vector<uint8_t>, a std::array of a given size, or something like a C-style array, and the template parameters cover the container size and the field count.
The examples in the README show the same call written four ways against four containers, which is the practical point about the output being generic:
uint8_t buffer[10];
auto bytes_written = serialize(object, buffer);The remaining three are a std::array, a std::vector and a std::ofstream opened in binary mode. So the same struct and the same call produce a stack buffer, a fixed array, a growable buffer and a file. There is no separate file-writing API, because the file is just another container.
Deserialization works in the same shape, taking a container and producing a value, with the error code out parameter for failure.
Now, the boilerplate claim. It is true that you do not write field lists or macros. But the number of fields, N, is a template parameter, and the note says it need not be provided under most circumstances. So the boilerplate has not vanished, it has moved into a template argument that the compiler usually works out. In the case of std::optional the compiler cannot, and you supply it.
That is a fair characterisation of the whole approach. The library does the reflection, and the price is that in a handful of cases you have to tell it something it cannot deduce. The alternative designs, in which you declare the fields yourself, make you write that information always. Which trade you prefer depends on whether your structs change often and whether a build step bothers you.
The samples directory in the repository is organised by topic, with separate files for arrays and vectors and strings, chrono durations, the demo, fundamental types, maps and sets, nested structures, optional values, unique pointers, variants, time_t and writing to a file, plus a CMakeLists.txt. So each supported type has a runnable example rather than only a line of documentation, and the pyalpaca_demo.py sample covers the Python side.
Python interoperability, and how to evaluate it
The Python support is described as experimental, with a pybind11-based wrapper module called pyalpaca, and the table of contents gives it three subsections: usage, format string specification, and two examples, one serializing and deserializing in Python and one serializing in C++ and deserializing in Python.
That second example is the one that matters. If a C++ program can write a buffer and a Python program can read it, then the two languages can exchange data without a text format in between, and you can keep a hot path in C++ while doing the analysis in Python. The format string specification subsection suggests there is a way to name the format so both sides agree on the layout, which is the necessary part of making that work.
The experimental label is the caveat. The library also has a Python wrapper in the repository, with the python/ directory at the top level and the pyalpaca_demo.py sample, and pybind11 appears in the topic list, along with python-wrapper and python3.
For the rest of the project, the repository layout is conventional: include/ for the headers, samples/, test/, benchmark/ with a benchmark_results.txt alongside it, cmake/ and a CMakeLists.txt, packaging/, python/, and a .clang-format with a clang_format.bash script. There is a .gitmodules, so some dependency is pulled in as a submodule.
So how should you evaluate this? Four things.
First, whether reflection is acceptable in your codebase. Some organisations forbid it, some compilers or build configurations make it awkward, and if you cannot use it then the whole library is out regardless of how good it is. The no-macros, no-code-generation framing suggests the author is aware this is the main objection, and the header-only design means there is no build-system integration to fight over.
Second, the compile-time configuration. If you need endian independence, fixed-width encoding, versioning and checksums, that is four switches and you should decide deliberately. The versioning and checksum features are the ones that turn a serializer into something you can keep using across releases of your own code, and they are optional, so the default is not the safe choice for long-lived data.
Third, the type coverage against your actual data. The samples directory lists what is covered and the README claims broad support, but the std::optional field-count caveat tells you the library is not entirely free of sharp edges, and the recursive structure case is the other one to test early.
Fourth, the release history, which is unusual. v0.3.0 came on 2026-10-02, but v0.2.1 was in September 2022 and v0.2.0 a fortnight before that. So there was a long gap at version 0.2, and the project is now at 0.3.0 with the last push on 2026-10-05. It is not archived and it is being worked on, but the version number is still pre-1.0 and the release gaps are long, so if you adopt it you should pin a version and expect to carry patches for some platforms yourself.
Editorial conclusion
Adopt alpaca if you are writing C++17 and want serialization without macros, without a code generation step and without extra dependencies, and if reflection is acceptable in your codebase, since the entire design depends on it. Do not adopt it if your build forbids reflection, and do not assume the defaults are the safe choice for long-lived data, because the optional versioning and checksum features are off unless you turn them on, and a format written by one version of your structs will otherwise be read as valid nonsense by the next. Before you commit, read the samples directory for the type you actually need, since each case has a runnable example, expect to pass the field count explicitly for std::optional, and pin a version, because the release gaps have been long even though the project is not archived and work continues on master.
Frequently asked questions
What is the alpaca C++ library?
It is a header-only serialization library for modern C++ written in C++17, which packs structs into a compact byte-array without macros, boilerplate code, source code generation or external dependencies. It is MIT licensed, published as v0.3.0 in October 2026, and the last push to the repository was on 2026-10-05.
Which C++ types can alpaca serialize?
Basic data types, STL containers, unique pointers, recursive data structures, optionals, variants and more, with output to C-style arrays, std::array, std::vector or directly to files. The samples directory has a separate file for each case, including maps and sets, nested structures, optional values, chrono types and unique pointers.
What compile-time options does alpaca offer?
Little endian by default and configurable to big endian, variable-length encoding by default for large integer types and configurable to fixed-width, optional type hashing with data structure versioning that is recursively generated and checked during deserialization, optional integrity checking with checksums, macros to exclude STL data structures, and aligned memory access.
Does alpaca need a field count or macros?
Under most circumstances the number of fields in the struct need not be provided, and alpaca works out the layout by reflecting over the type. In certain use cases, for example std::optional, you do need to provide the field count for correct operation.
Can alpaca be used from Python?
There is experimental Python support through a pybind11-based wrapper module called pyalpaca, with a format string specification and examples covering serializing and deserializing in Python, and serializing in C++ then deserializing in Python. The repository carries a python/ directory and a pyalpaca_demo.py sample.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/p-ranav-alpaca)Was this useful?