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))
Previous
Previous

Copy-on-write Tuples

Next
Next

Telemetry Timers