JSON Serialization
Did you know that CAF can generate (and parse) JSON for any inspectable type?
Consider this simple data type for representing a user with a numerical ID, a user name and an optional email address:
#include "caf/json_reader.hpp"
#include "caf/json_writer.hpp"
struct user {
uint32_t id;
std::string name;
std::optional<std::string> email;
};
template <class Inspector>
bool inspect(Inspector& f, user& x) {
return f.object(x).fields(f.field("id", x.id),
f.field("name", x.name),
f.field("email", x.email));
}
CAF_BEGIN_TYPE_ID_BLOCK(example_app, caf::first_custom_type_id)
CAF_ADD_TYPE_ID(example_app, (user))
CAF_END_TYPE_ID_BLOCK(example_app)
The setup above really just follows the basic template for enabling CAF type
inspection: provide an inspect
overload and assign a type ID. For generating
us some JSON, all we need to do is passing an user
object to a json_writer
!
The JSON writer comes with a couple of configuration options. However, passing
an object to the inspector always uses apply
. For our example, we factor out
this step to a utility function that throws an exception on errors and otherwise
prints the generated JSON to the terminal:
template <class T>
void json_println(caf::json_writer& writer, const T& obj) {
if (!writer.apply(obj)) {
std::cerr << "failed to generate JSON output: "
<< to_string(writer.get_error()) << '\n';
throw std::logic_error("failed to generate JSON");
}
println(writer.str());
writer.reset();
}
Two notes on this functions: str
returns a string_view
to an internal buffer
and we must rewind the buffer (and the state) by calling reset
before applying
another object.
With this utility in place, we can print users in JSON format. The writer API
has two configuration options: indentation
and skip_empty_fields
. The former
is a numerical value that tells CAF whether it should break after each value and
how much indentation it should add on each level of nesting. Setting this to 0
(the default) disables indentation and results in a compact single-line output.
Setting the skip_empty_fields
to true
tells CAF to omit missing fields
completely (the default). Otherwise, CAF includes the field in the output and
assigns null
to it as shown in example (c) below.
Source Code
auto john = user{1234, "John Doe", std::nullopt};
caf::json_writer writer;
println("(a): compact output");
json_println(writer, john);
println("\n(b): indentation = 2");
writer.indentation(2);
json_println(writer, john);
println("\n(c): indentation = 2 && skip_empty_fields = false");
writer.skip_empty_fields(false);
json_println(writer, john);
Output
(a): compact output
{"@type": "user", "id": 1234, "name": "John Doe"}
(b): indentation = 2
{
"@type": "user",
"id": 1234,
"name": "John Doe"
}
(c): indentation = 2 && skip_empty_fields = false
{
"@type": "user",
"id": 1234,
"name": "John Doe",
"email": null
}
CAF can convert any inspectable type to JSON. This also applies to
caf::message
, which really is just a tuple. Since JSON has no notion of
tuples, the writer outputs CAF messages as lists:
Source Code
auto msg = caf::make_message(user{1234, "John Doe", std::nullopt},
user{2345, "Jane Doe", "jane@doe.public"});
caf::json_writer writer;
writer.indentation(2);
json_println(writer, msg);
Output
[
{
"@type": "user",
"id": 1234,
"name": "John Doe"
},
{
"@type": "user",
"id": 2345,
"name": "Jane Doe",
"email": "jane@doe.public"
}
]
One last thing. As you can see, CAF also adds an @type
annotation with the C++
class name. The main reason for this inclusion is to enable CAF to deserialize
its own JSON output again! For this, CAF includes the class json_reader
:
Source Code
caf::message msg;
caf::json_reader reader;
// Step 1: parse JSON to an internal buffer.
if (!reader.load(R"([{"@type": "user", "id": 1234, "name": "John Doe"}])")) {
std::cerr << "failed to parse JSON input: "
<< to_string(reader.get_error()) << '\n';
throw std::logic_error("failed to parse JSON");
}
// Step 2: try to deserialize a message from the parse JSON input.
if (reader.apply(msg)) {
println("parsed JSON: ", msg);
} else {
println("failed to parse JSON: ", reader.get_error());
}
Output
parsed JSON: message(user(1234, "John Doe", null))