reflect-cpp
reflect-cpp is a C++-20 library for fast serialization, deserialization and validation using reflection, similar to pydantic in Python, serde in Rust, encoding in Go or aeson in Haskell.
As the aforementioned libraries are among the most widely used in the respective languages, reflect-cpp fills an important gap in C++ development. It reduces boilerplate code and increases code safety.
Design principles for reflect-cpp include:
- Close integration with containers from the C++ standard library
- Close adherence to C++ idioms
- Out-of-the-box support for JSON
- Simple installation: If no JSON support is required, reflect-cpp is header-only. For JSON support, only a single source file needs to be compiled.
- Simple extendability to other serialization formats
- Simple extendability to custom classes
- Standard C++ only, no compiler-specific macros
Why do we need this?
Suppose your C++ program has complex data structures it needs to save and load. Or maybe it needs to interact with some kind of external API. If you do this the traditional way, you will have a lot of boilerplate code. This is annoying and error-prone.
reflect-cpp is not just a reflection library, it is for serialization, deserialization and validation through reflection.
That means that you can encode your requirements about the input data in the type system and have them validated upfront. This is why the library also includes algebraic data types like tagged unions and numerous validation routines. Having your requirements encoded in the type system is the most reliable way of ensuring they are met. If your requirements are not met, the user of your software gets a very clear error message. Encoding your requirements in the type system also makes it a lot easier for anyone reading your code.
This increases user experience and developer experience, it makes your code safer (fewer bugs) and more secure (less prone to malicious attacks).
For a more in-depth theoretical discussions of these topics, the following books are warmly recommended:
- Category Theory for Programmers by Bartosz Milewski (https://github.com/hmemcpy/milewski-ctfp-pdf/releases)
- Domain Modeling Made Functional by Scott Wlaschin
Simple Example
#include <rfl/json.hpp>
#include <rfl.hpp>
struct Person {
std::string first_name;
std::string last_name;
int age;
};
const auto homer =
Person{.first_name = "Homer",
.last_name = "Simpson",
.age = 45};
// We can now write into and read from a JSON string.
const std::string json_string = rfl::json::write(homer);
auto homer2 = rfl::json::read<Person>(json_string).value();
The resulting JSON string looks like this:
{"first_name":"Homer","last_name":"Simpson","age":45}
More Comprehensive Example
#include <iostream>
#include <rfl/json.hpp>
#include <rfl.hpp>
// Age must be a plausible number, between 0 and 130. This will
// be validated automatically.
using Age = rfl::Validator<int, rfl::Minimum<0>, rfl::Maximum<130>>;
struct Person {
rfl::Rename<"firstName", std::string> first_name;
rfl::Rename<"lastName", std::string> last_name = "Simpson";
std::string town = "Springfield";
rfl::Timestamp<"%Y-%m-%d"> birthday;
Age age;
rfl::Email email;
std::vector<Person> children;
};
const auto bart = Person{.first_name = "Bart",
.birthday = "1987-04-19",
.age = 10,
.email = "[email protected]"};
const auto lisa = Person{.first_name = "Lisa",
.birthday = "1987-04-19",
.age = 8,
.email = "[email protected]"};
const auto maggie = Person{.first_name = "Maggie",
.birthday = "1987-04-19",
.age = 0,
.email = "[email protected]"};
const auto homer =
Person{.first_name = "Homer",
.birthday = "1987-04-19",
.age = 45,
.email = "[email protected]",
.children = std::vector<Person>({bart, lisa, maggie})};
// We can now transform this into a JSON string.
const std::string json_string = rfl::json::write(homer);
std::cout << json_string << std::endl;
// We can also directly write into std::cout (or any other std::ostream).
rfl::json::write(homer, std::cout) << std::endl;
This results in the following JSON string:
{"firstName":"Homer","lastName":"Simpson","town":"Springfield","birthday":"1987-04-19","age":45,"email":"[email protected]","children":[{"firstName":"Bart","lastName":"Simpson","town":"Springfield","birthday":"1987-04-19","age":10,"email":"[email protected]","children":[]},{"firstName":"Lisa","lastName":"Simpson","town":"Springfield","birthday":"1987-04-19","age":8,"email":"[email protected]","children":[]},{"firstName":"Maggie","lastName":"Simpson","town":"Springfield","birthday":"1987-04-19","age":0,"email":"[email protected]","children":[]}]}
We can also create structs from the string:
auto homer2 = rfl::json::read<Person>(json_string).value();
// Fields can be accessed like this:
std::cout << "Hello, my name is " << homer.first_name() << " "
<< homer.last_name() << "." << std::endl;
// Since homer2 is mutable, we can also change the values like this:
homer2.first_name = "Marge";
std::cout << "Hello, my name is " << homer2.first_name() << " "
<< homer2.last_name() << "." << std::endl;
Error messages
reflect-cpp returns clear and comprehensive error messages:
const std::string faulty_json_string =
R"({"firstName":"Homer","lastName":12345,"town":"Springfield","birthday":"04/19/1987","age":145,"email":"homer(at)simpson.com"})";
const auto result = rfl::json::read<Person>(faulty_json_string);
Yields the following error message:
Found 5 errors:
1) Failed to parse field 'lastName': Could not cast to string.
2) Failed to parse field 'birthday': String '04/19/1987' did not match format '%Y-%m-%d'.
3) Failed to parse field 'age': Value expected to be less than or equal to 130, but got 145.
4) Failed to parse field 'email': String 'homer(at)simpson.com' did not match format 'Email': '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'.
5) Field named 'children' not found.
Enums
reflect-cpp supports scoped enumerations:
enum class Color { red, green, blue, yellow };
struct Circle {
float radius;
Color color;
};
const auto circle = Circle{.radius = 2.0, .color = Color::green};
rfl::json::write(circle);
This results in the following JSON string:
{"radius":2.0,"color":"green"}
Algebraic data types
reflect-cpp supports Pydantic-style tagged unions, which allow you to form algebraic data types:
struct Circle {
double radius;
};
struct Rectangle {
double height;
double width;
};
struct Square {
double width;
};
using Shapes = rfl::TaggedUnion<"shape", Circle, Square, Rectangle>;
const Shapes r = Rectangle{.height = 10, .width = 5};
const auto json_string = rfl::json::write(r);
This results in the following JSON string:
{"shape":"Rectangle","height":10.0,"width":5.0}
Other forms of tagging are supported as well. Refer to the documentation for details.
Reflective programming
Beyond serialization and deserialization, reflect-cpp also supports reflective programming in general.
For instance:
struct Person {
std::string first_name;
std::string last_name = "Simpson";
std::string town = "Springfield";
unsigned int age;
std::vector<Person> children;
};
const auto fields = rfl::fields<Person>();
std::cout << "Fields in " << rfl::type_name_t<Person>().str() << ":"
<< std::endl;
for (const auto& f : fields) {
std::cout << "name: " << f.name() << ", type: " << f.type() << std::endl;
}
It also possible to replace fields:
struct Person {
std::string first_name;
std::string last_name;
std::vector<Person> children;
};
const auto lisa = Person{.first_name = "Lisa", .last_name = "Simpson"};
// Returns a deep copy of "lisa" with the first_name replaced.
const auto maggie = rfl::replace(
lisa, rfl::make_field<"first_name">(std::string("Maggie")));
Or you can create structs from other structs:
struct A {
std::string f1;
std::string f2;
};
struct B {
std::string f3;
std::string f4;
};
struct C {
std::string f1;
std::string f2;
std::string f4;
};
const auto a = A{.f1 = "Hello", .f2 = "World"};
const auto b = B{.f3 = "Hello", .f4 = "World"};
// f1 and f2 are taken from a, f4 is taken from b, f3 is ignored.
const auto c = rfl::as<C>(a, b);
You can also replace fields in structs using fields from other structs:
const auto a = A{.f1 = "Hello", .f2 = "World"};
const auto c = C{.f1 = "C++", .f2 = "is", .f4 = "great"};
// The fields f1 and f2 are replaced with the fields f1 and f2 in a.
const auto c2 = rfl::replace(c, a);
Support for containers
C++ standard library
reflect-cpp supports the following containers from the C++ standard library:
std::array
std::deque
std::forward_list
std::map
std::multimap
std::multiset
std::list
std::optional
std::pair
std::set
std::shared_ptr
std::string
std::tuple
std::unique_ptr
std::unordered_map
std::unordered_multimap
std::unordered_multiset
std::unordered_set
std::variant
std::vector
Additional containers
In addition, it supports the following custom containers:
rfl::Box
: Similar tostd::unique_ptr
, but (almost) guaranteed to never be null.rfl::Literal
: An explicitly enumerated string.rfl::NamedTuple
: Similar tostd::tuple
, but with named fields that can be retrieved via their name at compile time.rfl::Ref
: Similar tostd::shared_ptr
, but (almost) guaranteed to never be null.rfl::Result
: Allows for exception-free programming.rfl::TaggedUnion
: Similar tostd::variant
, but with explicit tags that make parsing more efficient.rfl::Validator
: Allows for automatic input validation.rfl::Variant
: An alternative tostd::variant
.
Custom classes
Finally, it is very easy to extend full support to your own classes, refer to the documentation for details.
Serialization formats
reflect-cpp currently supports the following serialization formats:
- JSON: Out-of-the-box support, no additional dependencies required.
- flexbuffers: Requires flatbuffers.
reflect-cpp is deliberately designed in a very modular format, using concepts, to make it as easy as possible to support additional serialization formats. Refer to the documentation for details. PRs related to serialization formats are welcome.
Documentation
Click here.
Installation
The following compilers are supported:
- GCC 11.4 or higher
- Clang 16.0 or higher
- MSVC 17.8 or higher
Option 1: Header-only
If you do not need JSON support or you want to link YYJSON yourself, then reflect-cpp is header-only. Simply copy the contents of the folder include
into your source repository or add it to your include path.
Option 2: Include source files into your own build
Simply copy the contents of the folder include
into your source repository or add it to your include path and also add src/yyjson.c
to your source files for compilation.
If you need support for other serialization formats like flexbuffers, you should also include and link the respective libraries, as listed in the previous section.
Option 3: Compilation using cmake
This will simply compile YYJSON, which is the JSON library underlying reflect-cpp. You can then include reflect-cpp in your project and link to the binary to get reflect-cpp with JSON support.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 4 # gcc, clang
cmake --build build --config Release -j 4 # MSVC
Option 4: Compilation using cmake and vcpkg
If you want serialization formats other than JSON, you can either install them manually or use vcpkg.
To install vcpkg:
git submodule update --init
./vcpkg/bootstrap-vcpkg.sh # Linux, macOS
./vcpkg/bootstrap-vcpkg.bat # Windows
# You may be prompted to install additional dependencies.
To use reflect-cpp in your project:
add_subdirectory(reflect-cpp) # Add this project as a subdirectory
set(REFLECTCPP_FLEXBUFFERS ON) # Optional
target_link_libraries(your_project PRIVATE reflectcpp) # Link against the library
Troubleshooting vcpkg
vcpkg is a great, but very ambitious and complex project (just like C++ is a great, but very ambitious and complex language). Here are some of the you might run into and how to resolve them:
-
A lot of problems can simply be resolved by deleting the build directory using
rm -rf build
. -
Environment variable VCPKG_FORCE_SYSTEM_BINARIES must be set on arm, s390x, ppc64le and riscv platforms. - This usually happens on arm platforms like the Apple Silicon chips and can be resolved by simply preceding your build with
export VCPKG_FORCE_SYSTEM_BINARIES=arm
orexport VCPKG_FORCE_SYSTEM_BINARIES=1
. -
On some occasions you might be asked to specify a compiler. You can do so by simply adding it to the cmake command as follows:
cmake -S . -B build ... -DCMAKE_C_COMPILER=gcc -DCMAKE_C_COMPILER=g++
orcmake -S . -B build ... -DCMAKE_C_COMPILER=clang-17 -DCMAKE_C_COMPILER=clang++-17
(or whatever supported compiler you would like to use).
Compiling the tests
To compile the tests, do the following:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DREFLECTCPP_BUILD_TESTS=ON
cmake --build build -j 4 # gcc, clang
cmake --build build --config Release -j 4 # MSVC
./build/tests/json/reflect-cpp-json-tests
To compile the tests with serialization formats other than JSON, do the following:
# bootstrap vcpkg if you haven't done so already
git submodule update --init
./vcpkg/bootstrap-vcpkg.sh # Linux, macOS
./vcpkg/bootstrap-vcpkg.bat # Windows
# You may be prompted to install additional dependencies.
cmake -S . -B build -DREFLECTCPP_BUILD_TESTS=ON -DREFLECTCPP_FLEXBUFFERS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 4 # gcc, clang
cmake --build build --config Release -j 4 # MSVC
./build/tests/flexbuffers/reflect-cpp-flexbuffers-tests
./build/tests/json/reflect-cpp-json-tests
Related projects
reflect-cpp was originally developed for getml-community, the fastest open-source tool for feature engineering on relational data and time series. If you are interested in Data Science and/or Machine Learning, please check it out.
Authors
reflect-cpp has been developed by scaleML, a company specializing in software engineering and machine learning for enterprise applications. It is extensively used for getML, a software for automated feature engineering using relational learning.
License
reflect-cpp is released under the MIT License. Refer to the LICENSE file for details.
reflect-cpp includes YYJSON, the fastest JSON library currently in existence. YYJSON is written by YaoYuan and also released under the MIT License.