Skip to content

rfl::Generic

reflect-cpp is intended to be used to directly parse into structs. Doing this is more efficient, more convenient and also safer.

But in some cases, we simply cannot anticipate the complete structure of the data at compile time. For these cases, we have rfl::Generic.

rfl::Generic is a convenience wrapper around the following type:

std::variant<bool, int, double, std::string, Object, Array, std::nullopt_t>;

Object and Array are defined as follows:

using Array = std::vector<Generic>;
using Object = rfl::Object<Generic>;

In other words, rfl::Generic encompasses all types that can be expressed in JSON.

It can be used as follows:

auto bart = rfl::Generic::Object();
bart["first_name"] = "Bart";
bart["last_name"] = "Simpson";
bart["age"] = 10;

auto lisa = rfl::Generic::Object();
lisa["first_name"] = "Lisa";
lisa["last_name"] = "Simpson";
lisa["age"] = 8;

auto maggie = rfl::Generic::Object();
maggie["first_name"] = "Lisa";
maggie["last_name"] = "Simpson";
maggie["age"] = 0;

auto homer = rfl::Generic::Object();
homer["first_name"] = "Homer";
homer["last_name"] = "Simpson";
homer["age"] = 45;
homer["children"] = rfl::Generic::Array({bart, lisa, maggie});

The resulting JSON strings looks as follows:

{"first_name":"Homer","last_name":"Simpson","age":45,"children":[{"first_name":"Bart","last_name":"Simpson","age":10},{"first_name":"Lisa","last_name":"Simpson","age":8},{"first_name":"Lisa","last_name":"Simpson","age":0}]}

rfl::Generic contains some convenience methods that allow you to handle parsed data:

You can retrieve the underlying std::variant using .get(). This then allows you to handle allow possible seven cases using the visitor pattern.

If you have a guess what the types of a particular field might be, you can use any of the seven convenience methods .to_array(), .to_bool(), .to_double(), .to_int(), .to_null(), .to_object(), .to_string(). Each of these methods will return an rfl::Result<...> with the corresponding type, if the rfl::Generic does indeed contain such a type.

If you prefer, you can also use the following seven convenience functions:

rfl::to_array(...), rfl::to_bool(...), rfl::to_double(...), rfl::to_int(...), rfl::to_null, rfl::to_object(...), rfl::to_string(...).

These functions are particularly useful for monadic error handling:

auto homer = rfl::Generic::Object();
...
rfl::Result<int> age = homer.get("age").and_then(rfl::to_int);

In this particular instance, we know that homer is an rfl::Generic::Object. We assume that it has a field named age which we also assume is an integer. In the end, we will get an rfl::Result<int> which will contain an integer, if both of our assumptions are correct and an error otherwise.

If we want to retrieve the age, we can use the .value() method:

auto homer = rfl::Generic::Object();
...
int age = homer.get("age").and_then(rfl::to_int).value();

This will throw an exception if any of our assumptions are false.

Please refer to the part of the documentation on rfl::Result to learn more about monadic error handling.

rfl::to_generic and rfl::from_generic

You can transform any struct that can be converted to a JSON to the equivalent generic instead.

In other words:

assert(rfl::json::write(my_struct) == rfl::json::write(rfl::to_generic(my_struct)));

And:

assert(rfl::json::read<rfl::Generic>(rfl::json::write(my_struct)).value() == rfl::to_generic(my_struct));

What is more, you can apply any processors you want to:

assert(rfl::json::write<rfl::SnakeCaseToCamelCase>(my_struct) == rfl::json::write(rfl::to_generic<rfl::SnakeCaseToCamelCase>(my_struct)));

assert(rfl::json::read<rfl::Generic>(rfl::json::write<rfl::SnakeCaseToCamelCase>(my_struct)).value() == rfl::to_generic<rfl::SnakeCaseToCamelCase>(my_struct));

Conversely, the same statements hold for reading. In other words:

assert(rfl::json::read<MyStruct>(my_json_string).value() == rfl::from_generic<MyStruct>(rfl::json::read<rfl::Generic>(my_json_string).value()).value());

Likewise, you can apply processors:

assert(rfl::json::read<MyStruct, rfl::SnakeCaseToCamelCase>(my_json_string).value() == rfl::from_generic<MyStruct, rfl::SnakeCaseToCamelCase>(rfl::json::read<rfl::Generic>(my_json_string).value()).value());

In fact, you can think of rfl::from_generic and rfl::to_generic as manifestations of the reader and writer interface. You could as well write rfl::generic::read<...>(...) and rfl::generic::write(...).

This can be used to dynamically build structs:

struct Person {
    std::string first_name;
    std::string last_name;
    int age;
};

auto bart = rfl::Generic::Object();
bart["first_name"] = "Bart";
bart["last_name"] = "Simpson";
bart["age"] = 10;

const auto person = rfl::from_generic<Person>(bart).value();