Open-source project
xyz347/xpack avatar
xyz347/xpack

xpack turns a C++ struct into JSON, XML, YAML, BSON or a database row

convert json/xml/bson to c++ struct

663 stars133 forksC++Apache-2.0

At a glance

What is it?
xpack is a header-only C++ library that serialises a plain struct to and from JSON, XML, YAML, BSON, MySQL and SQLite, driven by a macro placed at the end of the struct. The JSON path is the mature one, the optional backends are documented as insufficiently tested, and the last release tag is two years older than the last commit.
Who is it for?
xpack is worth adopting for a C++ codebase that has to speak several serialisation formats from the same struct definitions, especially where a generated serializer would mean a build step, since xpack needs no library file and no Makefile, only headers. It is the wrong tool if you need a database backend you can trust on day one, because the MySQL, SQLite, YAML and BSON paths are all documented as not fully tested and each pulls in its own system dependency.
Can I use it commercially?
Yes. Apache-2.0 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 131 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 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A macro at the end of the struct drives the whole conversion

xpack is a collection of header files that convert between C++ structs and json, xml, yaml, bson, mysql and sqlite. The distribution is header only, there is no library file to compile, and consequently no Makefile for the library itself.

The mechanism is a macro appended to the struct definition. Each letter inside the macro selects a different kind of member handling, and the two calls you make are xpack::json::encode to turn a struct into a JSON string and xpack::json::decode to fill a struct from one. The header you include depends on the format, with xpack/json.h for JSON and xpack/xml.h for XML.

A minimal program looks like this:

cpp
#include <iostream>
#include "xpack/json.h"

using namespace std;

and the struct itself carries the macro at the end of its definition, listing the members the serialiser should touch.

The member types that are supported out of the box are the standard containers: vector, set, list, map keyed by string, map keyed by an integer, which is JSON only because XML does not support it, unordered_map keyed by string and shared_ptr, the last two needing C++11 support. A struct with an unsupported member type is where the custom codec machinery comes in, and the example directory in the repository carries a file for each of these features, from alias.cpp and bitfield.cpp through union1.cpp, json-data.cpp and xtype_advance.cpp.

Flags carry the behaviour, from omitempty to XML attributes

Inside the macro, the letter X carries a list of flags in an F group followed by the members. The flags that exist are worth knowing by name because they cover most of what people hand-roll on top of a serialiser.

Flag 0 means no flags at all. OE is omitempty: when encoding, a member that is zero, an empty string or false produces no key in the output. EN is empty as null, and the difference from OE is precise: OE omits the field entirely while EN emits a null for it. M is mandatory, and on decode a missing field throws an exception, which the documentation suggests for identifier fields where absence is an error rather than a default.

The last two flags are format specific. ATTR puts the value into an XML attribute when encoding XML rather than into the element body. SL is single line, which puts a JSON array on one line instead of splitting it across lines.

The single-letter shortcuts are conveniences over X. O is X with no flags, M is X with the mandatory flag, and there is a C form for a custom codec and A and AF for aliases. The practical value of the flag system is that it keeps the policy next to the member, so the rule that an id must be present travels with the declaration of that id.

Aliases, bitfields and inheritance each have their own letter

Three of the macro letters handle structural mismatches between C++ and the wire format, and each carries a rule you have to know.

A handles the case where the member name and the key name differ, and AF is the same with flags attached. The alias itself has a format of "x t:n", where x is a global alias, t is the type and n is the alias under that type. The documentation is explicit that the global part may be missing, so json:_id is legal, and that the type part may be missing too, so _id is legal. When both are present the type-specific alias wins, otherwise the global one applies, and otherwise the member name is used.

B covers bitfields, and the constraint is that bitfields do not support aliases:

cpp
struct Test {
    short ver:8;
    short len:8;
    string  name;
    XPACK(B(F(0), ver, len), O(name)); 

I covers inheritance, with the base classes listed inside it. Two rules make this more work than it looks. The base classes of the base classes have to be listed as well, so a three level hierarchy needs all the ancestors in the leaf, and any class used as a base must define the XPACK or XPACK_OUT macro itself.

E is for enumerations, and its role has been reduced over time. With a C++11 compiler an enum behaves like an ordinary member and can go inside X, O, M or A. Without C++11 it has to be declared through E, and in that form it does not support aliases.

Custom codecs exist at two levels, and one of them wins

Some types cannot be serialised member by member, and xpack offers two mechanisms with a defined precedence.

The first is xtype, which is type level. Once a type is wrapped, the custom encode and decode functions apply to that type everywhere it appears, with the exception that xtype cannot be applied to basic types such as int and string. The second is the C form of the macro, C(customcodec, F(flags...), members...), which is member level. It works for basic types as well as for other types, but it only applies to the members it lists, so a member left in O keeps the default behaviour while the member inside C gets the custom one.

The precedence rules are stated and are useful. xtype takes priority over the XPACK macro, so a type with an xtype definition uses that definition by default, but the C method takes priority over xtype, so a member listed inside C always uses the codec named there. The example xtype_advance.cpp uses both together to encode differently depending on the value of another member.

One detail makes custom codecs practical rather than a dead end. The XPACK macro adds decode and encode functions to the struct named __x_pack_decode and __x_pack_encode, and a custom codec can call those to fall back to the default behaviour for the parts it does not want to handle itself. Unions are handled this way as well, with example/union1.cpp showing the pattern.

The optional backends each need a system library and are barely tested

The format support is not uniform, and the documentation does not pretend otherwise.

JSON and XML are the core, and they are what the examples and the quick start focus on. Beyond those, each additional format requires a system library that you have to install yourself. BSON depends on libbson-1.0, MySQL depends on libmysqlclient-dev, SQLite depends on libsqlite3 and YAML depends on yaml-cpp. All four carry the same warning in the README, that they are not sufficiently tested.

That warning is the single most useful line in the documentation for anyone choosing this library. A JSON round trip in a struct is easy to trust because the whole design is oriented around it, with flags such as omitempty and single line defined in terms of JSON encoding. A MySQL row mapping that has not been tested is a different proposition, and the same applies to the BSON path, which additionally has its own README file, README-bson.md, for the details.

The header layout mirrors this. json.h, xml.h, yaml.h, bson.h, mysql.h and sqlite.h sit at the top level, each pulling in a matching encoder and decoder, with json_data.h for the schemaless case and extend.h, numeric.h, traits.h and util.h as the shared machinery. RapidJSON and RapidXML are vendored in the repository, and a gtest directory is present for the tests.

The version picture deserves attention too. The licence is Apache-2.0, the last push to the repository was on 2026-06-14, and the most recent release tag is v1.0.6 from May 2024, with v1.0.5 and v1.0.4 before it. Commits have continued well past the last tag, so a consumer pinning a tag gets older code than the repository head.

Arrays truncate on decode and char arrays behave like C strings

Two array rules are documented, and both are the kind of detail that saves a debugging session.

When decoding, if the incoming array has more elements than the fixed array in the struct, the extra elements are dropped. There is no error and no exception, the array is simply filled up to its length. In the other direction a char array is treated as a null terminated string, so a JSON string longer than the buffer will be truncated at the terminator.

cpp
struct Test {
    char  name[64];
    char  email[64];
    XPACK(O(name, email)); 

The example file char_array.cpp covers the same ground, and msvc.make and makefile in the example directory show how the samples are built.

For a schema you do not know in advance, the library provides xpack::JsonData from json_data.h, which is designed for exactly the case where the JSON structure is uncertain. It exposes Type to inspect the kind of value, an IsXXX family of predicates, a GetXXX family for extraction, an overloaded bool to test whether the value is valid, Size for arrays, an operator[] taking a size_t for array elements and one taking a const char* for object members, and Begin, Next and Key for iterating the members of an object.

Third-party structs are handled with a different macro. XPACK_OUT is used instead of XPACK for a struct you do not own, and it has to be defined in the global namespace.

Editorial conclusion

xpack is worth adopting for a C++ codebase that has to speak several serialisation formats from the same struct definitions, especially where a generated serializer would mean a build step, since xpack needs no library file and no Makefile, only headers. It is the wrong tool if you need a database backend you can trust on day one, because the MySQL, SQLite, YAML and BSON paths are all documented as not fully tested and each pulls in its own system dependency. Verify first which formats your build actually needs, install the matching dependency such as yaml-cpp, libbson-1.0, libmysqlclient-dev or libsqlite3 yourself, and read the examples for the features you intend to use before you rely on them.

Frequently asked questions

How do I use xpack to convert a C++ struct to JSON?

Add the XPACK macro at the end of the struct definition listing the members, include xpack/json.h, then call xpack::json::encode for the struct and xpack::json::decode to fill one. xpack is header only, so there is no library file to compile and no Makefile.

Which formats does xpack support?

JSON and XML are the core. BSON, MySQL, SQLite and YAML are also supported but each needs a system library you install yourself, libbson-1.0, libmysqlclient-dev, libsqlite3 and yaml-cpp respectively, and the documentation marks all four as not sufficiently tested.

What do the XPACK macro flags do?

Inside the X form, flag 0 means no flags, OE is omitempty and skips zero, empty or false members, EN emits a null for empty values, M is mandatory and throws on decode when the field is missing, ATTR places the value in an XML attribute, and SL keeps a JSON array on a single line.

How do I give a struct member a different key name in xpack?

Use the A form for an alias or AF for an alias with flags. The alias format is "x t:n", where the type is one of json, xml or bson, the global part may be omitted as in json:_id and the type part may be omitted as in _id. A type-specific alias takes priority over a global one.

How does xpack handle a type it cannot serialise member by member?

Either with xtype, which applies to a whole type but not to basic types like int and string, or with the C form of the macro, which applies to the members it lists and does work for basic types. xtype takes priority over the XPACK macro, and the C form takes priority over xtype. Unions are handled through custom codecs as well.

What happens when xpack decodes an array that is longer than the struct field?

The extra elements are dropped, so the array is filled up to its own length without an error. A char array is treated as a null terminated string, so longer text is truncated at the terminator.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. xyz347/xpack on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/xyz347-xpack.svg)](https://hysenlabs.com/projects/xyz347-xpack)

Was this useful?