Examples » Core » Data » Serialization » Custom Serializer

Implementing your own Serializer.

To define your own serializer type, you'll need to include core/data/ser/serializer.hpp. For simplicity, in this sample we'll use the following aliases:

#include <cubos/core/data/ser/serializer.hpp>

using cubos::core::data::Serializer;
using cubos::core::reflection::Type;

We'll define a serializer that will print the data to the standard output.

class MySerializer : public Serializer
{
public:
    MySerializer();

protected:
    bool decompose(const Type& type, const void* value) override;
};

In the constructor, we should set hooks to be called for serializing primitive types or any other type we want to handle specifically.

In this example, we'll only handle int32_t, but usually you should at least cover all primitive types.

#include <cubos/core/reflection/external/primitives.hpp>

using cubos::core::reflection::reflect;

MySerializer::MySerializer()
{
    this->hook<int32_t>([](const int32_t& value) {
        Stream::stdOut.print(value);
        return true;
    });
}

The only other thing you need to do is implement the Serializer::decompose method, which acts as a catch-all for any type without a specific hook.

Here, we can use traits such as FieldsTrait to get the fields of a type and print them.

In this sample, we'll only be handling fields and arrays, but you should try to cover as many kinds of data as possible.

#include <cubos/core/reflection/traits/array.hpp>
#include <cubos/core/reflection/traits/fields.hpp>
#include <cubos/core/reflection/type.hpp>

using cubos::core::reflection::ArrayTrait;
using cubos::core::reflection::FieldsTrait;

bool MySerializer::decompose(const Type& type, const void* value)
{
    if (type.has<ArrayTrait>())
    {
        const auto& arrayTrait = type.get<ArrayTrait>();

        Stream::stdOut.put('[');
        for (const auto* element : arrayTrait.view(value))
        {
            if (!this->write(arrayTrait.elementType(), element))
            {
                return false;
            }
            Stream::stdOut.print(", ");
        }
        Stream::stdOut.put(']');

        return true;
    }

We start by checking if the type can be viewed as an array. If it can, we recurse into its elements. Otherwise, we'll fallback to the fields of the type.

    if (type.has<FieldsTrait>())
    {
        Stream::stdOut.put('{');
        for (const auto& [field, fieldValue] : type.get<FieldsTrait>().view(value))
        {
            Stream::stdOut.printf("{}: ", field->name());
            if (!this->write(field->type(), fieldValue))
            {
                return false;
            }
            Stream::stdOut.print(", ");
        }
        Stream::stdOut.put('}');

        return true;
    }

    CUBOS_WARN("Cannot decompose {}", type.name());
    return false;
}

If the type has fields, we'll iterate over them and print them. Otherwise, we'll fail by returning false.

Using our serializer is as simple as constructing it and calling Serializer::write on the data we want to serialize.

In this case, we'll be serializing a std::vector<glm::ivec3>, which is an array of objects with three int32_t fields.

#include <glm/vec3.hpp>

#include <cubos/core/reflection/external/glm.hpp>
#include <cubos/core/reflection/external/vector.hpp>

int main()
{
    std::vector<glm::ivec3> vec{{1, 2, 3}, {4, 5, 6}};

    MySerializer ser{};
    ser.write(vec);
}

This should output:

// [{x: 1, y: 2, z: 3, }, {x: 4, y: 5, z: 6, }, ]