Iros
 
Loading...
Searching...
No Matches
Serialization

Purpose

The library aims to provide an extremely convient way to serialize and deserialize objects, leveraging static reflection to provide a simple interface.

Usage

Consider a normal type MyType which has static reflection added. This can be serialized as follows:

struct MyType {
int x;
int y;
int z;
constexpr friend auto tag_invoke(di::Tag<di::reflect>, di::InPlaceType<MyType>) {
);
}
};
di::meta::Decay< decltype(T)> Tag
Definition tag_invoke.h:28
constexpr auto field
Definition field.h:42
constexpr auto make_tuple(Args &&... args)
Definition make_tuple.h:9
Definition in_place_type.h:5
auto x = MyType { 1, 2, 3 };
// Serialize to JSON string:
auto string = TRY(di::to_json_string(x));
// or...
// Serialize to JSON file (with pretty printing):
auto file = TRY(dius::open_sync("file.json", dius::OpenMode::WriteOnly | dius::OpenMode::Create));
// or ...
// or ...
auto serializer = di::JsonSerializer(di::move(file), di::JsonSerializerConfig().pretty());
TRY(di::serialize(serializer, x));
Definition json_serializer.h:36
Definition json_serializer.h:61
#define TRY
Definition monad_try.h:23
constexpr auto serialize_string
Definition serialize_string.h:23
constexpr auto serialize
Definition serialize.h:130
constexpr auto json_format
Definition json_serializer.h:371
constexpr auto serialize_json
Definition json_serializer.h:396
constexpr auto to_json_string
Definition json_serializer.h:383
auto open_sync(di::PathView path, OpenMode open_mode, u16 create_mode=0666) -> di::Expected< SyncFile, di::GenericCode >
Definition sync_file.cpp:76

The library also supports deserialization using a mostly symmetric interface. For example:

// Deserialize from JSON string:
auto x = TRY(di::from_json_string<MyType>("{ x: 1, y: 2, z: 3 }"_sv));
// or...
auto x = TRY(di::deserialize_string<MyType>(di::json_format, "{ x: 1, y: 2, z: 3 }"_sv));
// Deserialize from JSON file:
auto file = TRY(dius::open_sync("file.json", dius::OpenMode::ReadOnly));
// or...
// or...
auto deserializer = di::JsonDeserializer(di::move(file));
auto x = TRY(di::deserialize(deserializer));
A deserializer for the JSON format.
Definition json_serializer.h:355
constexpr auto from_json_string
Definition json_deserializer.h:510
constexpr auto deserialize_json
Definition json_deserializer.h:526
constexpr auto deserialize_string
Definition deserialize_string.h:27
constexpr auto deserialize
Definition deserialize.h:178

Using the JSON serializer, enumerations can also be serialized to strings using static reflection. Additionally, users can provide custom serialization behavior by overriding the di::serialize_metadata function. For instance:

#include "di/serialization/prelude.h"
struct MyType {
int x_abc;
int y_abc;
int z_abc;
constexpr friend auto tag_invoke(di::Tag<di::serialize_metadata>,
);
}
};

This allows overriding the names or only serializing a subset of the fields. This can be done on a per-format basis or for all formats at once. When resolving how to serialize a type, the following order is used:

  1. If the type has a di::serialize_metadata overload for the given format, use that.
  2. If the type has a blanket di::serialize_metadata overload, use that.
  3. If the type has a di::reflect overload, use that.

This metadata search is handled by the di::serialize function, which evaluates the metadata and then calls the provided serializer. This dispatching mechanism ensures that serializers will have consistent behavior across all implementations.

Additionally, di::serialize() is a tag-invoke CPO, so the entire serialization mechanism can be customized on a per-type basis if needed.

For example,

struct UUID {
// Returns fixed-size string representation of the UUID.
auto to_string() const;
// Custom serialization. For JSON, we would want a string, but for binary formats we would want a fixed-size array.
friend auto tag_invoke(di::Tag<di::serialize>, di::JsonFormat, auto& serializer, UUID const& value) {
return serializer.serialize_string(value.to_string());
}
};
Definition json_serializer.h:357
Definition span_fixed_size.h:37

The deserialization mechanism has equivalent customization points, like di::deserialize_metadata and di::deserialize. However, they take di::InPlaceType<T> as arguments instead of the actual value to be serialized. Additionally, di::deserialization_metadata will fallback to di::serialize_metadata if no deserialization metadata is provided.

Custom Serialization Formats

The library is designed to be extensible to support multiple serialization formats in the future. A serialization format is modelled by the di::SerializationFormat concept, which is defined as follows:

template<typename T, typename Writer = di::AnyRef<io::Writer>, typename... Args>
concept SerializationFormat = requires(T format, Writer&& writer, Args&&... args) {
serialization::serializer(format, di::forward<Writer>(writer), di::forward<Args>(args)...);
};

As shown, a serialization format must provide an implementation of the serializer function which takes a writer, and returns a bound serializer. This can be done by providing a static function serializer. This should be valid for any type of writer, but since c++ concepts cannot universality, a type-erased Writer is checked. The serializer must be a type which models the di::Serializer concept, which is defined as follows:

template <typename T>
concept Serializer = requires(T& serializer, DummyType value, DummyMetadata metadata) {
typename SerializationFormat;
{ serializer.serialize(value, metadata) } -> ConvertibleTo<di::Result<void>>;
{ serializer.writer() } -> di::Impl<Writer>;
di::as_const(serializer).writer();
di::move(serializer).writer();
};

The metadata object passed to the serialization is either a list of fields or a atom, depending on the type being serialized. Different serializers may be able to serialize different values, so the second constraint on Serializer is not enforced. Instead, the important concept is one that considers both the serializer and the type being serialized:

template<typename T, typename S>
concept Serializable =
di::Serializer<S> && requires(S&& serializer, T&& value) { di::serialize(serializer, value); };

Custom Deserialization Formats

The deserialization mechanism is similar to the serialization mechanism, but with a few key differences. First, the di::Deserializer concept replaces the writer() function with a reader() function. Second, the di::Deserializer concept is not constrained by the SerializationFormat concept. Instead, it is constrained by the DeserializationFormat concept, which is defined as follows:

template<typename T, typename Reader = di::AnyRef<io::Reader>, typename... Args>
concept DeserializationFormat = requires(T format, Reader&& reader, Args&&... args) {
serialization::deserializer(format, di::forward<Reader>(reader), di::forward<Args>(args)...);
};

To be a useful di::Deserializer, the type must overload the deserialize() function to accept an InPlaceType<T> to be deserialized. Optionally, it can also accept a metadata object for the type being deserialized.