Skip to main content
Basis uses a plugin-based serialization system. Each message type is associated with exactly one serializer. The serializer is selected at compile time through a template specialization mechanism — there is no runtime dispatch for the hot path. Serialization only occurs when a message crosses a network or disk boundary; inproc delivery passes a shared_ptr directly with zero copying.

The Serializer interface

All serializers inherit from basis::core::serialization::Serializer and implement three static template methods:
// basis/core/serialization/include/basis/core/serialization.h

class Serializer {
  // Returns the number of bytes needed to serialize `message`.
  template <typename T_MSG>
  static size_t GetSerializedSize(const T_MSG& message);

  // Writes `message` into the region pointed to by `span`.
  // Returns true on success.
  template <typename T_MSG>
  static bool SerializeToSpan(const T_MSG& message, std::span<std::byte> span);

  // Reads a message from `bytes`.
  // Returns a heap-allocated message on success, nullptr on failure.
  template <typename T_MSG>
  static std::unique_ptr<T_MSG> DeserializeFromSpan(std::span<const std::byte> bytes);
};
In addition to the three core methods, serializers that support schema introspection and MCAP recording implement:
// Dumps the schema metadata for T_MSG (used by the recorder).
template <typename T_MSG>
static basis::core::serialization::MessageSchema DumpSchema();

// Returns type identification used for wire encoding and MCAP.
template <typename T_MSG>
static basis::core::serialization::MessageTypeInfo DeduceMessageTypeInfo();

SerializationHandler<T> — type deduction

SerializationHandler<T_MSG> is the mechanism that maps a message type to its serializer. The primary template is intentionally left as a static_assert:
// basis/core/serialization.h

template <typename T_MSG, typename Enable = void>
struct SerializationHandler {
  using type = core::serialization::Serializer;

  static_assert(false,
    "Serialization handler for this type not found — "
    "please make sure you've included the proper serialization plugin");
};
Each plugin adds a partial specialization for its supported type family. Including the plugin header activates that specialization. You never instantiate SerializationHandler directly — you use it as a type alias:
typename SerializationHandler<MyMessage>::type serializer;
// or
auto msg = SerializationHandler<MyMessage>::type::template DeserializeFromSpan<MyMessage>(bytes);
The transport layer and code generator use this idiom throughout to remain generic.

Built-in plugins

Protobuf plugin

Header: basis/plugins/serialization/protobuf.h
YAML prefix: protobuf:
Serializer ID: "protobuf"
// basis/cpp/plugins/serialization/protobuf/include/basis/plugins/serialization/protobuf.h

class ProtobufSerializer : public core::serialization::Serializer {
public:
  static constexpr char SERIALIZER_ID[] = "protobuf";

  template <typename T_MSG>
  static bool SerializeToSpan(const T_MSG& message, std::span<std::byte> span) {
    return message.SerializeToArray(span.data(), span.size());
  }

  template <typename T_MSG>
  static size_t GetSerializedSize(const T_MSG& message) {
    return message.ByteSizeLong();
  }

  template <typename T_MSG>
  static std::unique_ptr<T_MSG> DeserializeFromSpan(std::span<const std::byte> bytes) {
    auto msg = std::make_unique<T_MSG>();
    if (!msg->ParseFromArray(bytes.data(), bytes.size())) return nullptr;
    if (!msg->IsInitialized()) return nullptr;
    return msg;
  }
};
The SerializationHandler specialization activates for any type that inherits from google::protobuf::Message:
template <typename T_MSG>
struct SerializationHandler<T_MSG,
    std::enable_if_t<std::is_base_of_v<google::protobuf::Message, T_MSG>>> {
  using type = plugins::serialization::protobuf::ProtobufSerializer;
};
Including protobuf.h is sufficient — you do not need to call any registration function. MCAP encoding: "protobuf" for both schema and message. Schema format: A serialized google::protobuf::FileDescriptorSet containing all transitive dependencies of the message descriptor. This allows the MCAP recorder to store enough information to decode messages without the original .proto files.

ROS1 msg plugin

Header: basis/plugins/serialization/rosmsg.h
YAML prefix: rosmsg:
Serializer ID: "rosmsg"
// basis/cpp/plugins/serialization/rosmsg/include/basis/plugins/serialization/rosmsg.h

class RosmsgSerializer : public core::serialization::Serializer {
public:
  static constexpr char SERIALIZER_ID[] = "rosmsg";

  template <typename T_MSG>
  static bool SerializeToSpan(const T_MSG& message, std::span<std::byte> span) {
    ros::serialization::OStream out(
        reinterpret_cast<uint8_t*>(span.data()), span.size());
    ros::serialization::serialize(out, message);
    return true;
  }

  template <typename T_MSG>
  static size_t GetSerializedSize(const T_MSG& message) {
    return ros::serialization::serializationLength(message);
  }

  template <typename T_MSG>
  static std::unique_ptr<T_MSG> DeserializeFromSpan(std::span<const std::byte> bytes) {
    auto msg = std::make_unique<T_MSG>();
    ros::serialization::IStream in(
        const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(bytes.data())),
        bytes.size());
    ros::serialization::deserialize(in, *msg);
    return msg;
  }
};
The specialization activates for any type that satisfies ros::message_traits::IsMessage<T>:
template <typename T_MSG>
struct SerializationHandler<T_MSG,
    std::enable_if_t<ros::message_traits::IsMessage<T_MSG>::value>> {
  using type = plugins::serialization::RosmsgSerializer;
};
MCAP encoding: "ros1" for messages, "ros1msg" for schemas. Schema format: The ROS message definition string, obtained via ros::message_traits::Definition<T>::value(). The MD5 hash is stored as basis_hash_id in MCAP channel metadata.

Raw serializer

YAML prefix: raw:
Class: basis::core::serialization::RawSerializer
Treats the message as a flat byte buffer using memcpy. Only safe for plain-old-data (POD) structs with no pointers. Does not support schema introspection or MCAP schema encoding.
class RawSerializer : public Serializer {
  template <typename T_MSG>
  static size_t GetSerializedSize(const T_MSG& message) { return sizeof(message); }

  template <typename T_MSG>
  static bool SerializeToSpan(const T_MSG& message, std::span<std::byte> span) {
    memcpy(span.data(), &message, sizeof(message));
    return true;
  }
};

Type identification on the wire — MessageTypeInfo

When a publisher advertises a topic, it registers the type’s identity with the coordinator and embeds it in network packets. The identity is described by MessageTypeInfo:
// basis/core/serialization/include/basis/core/serialization/message_type_info.h

struct MessageTypeInfo {
  std::string serializer;            // e.g. "protobuf", "rosmsg"
  std::string name;                  // e.g. "sensor_msgs.Image", "sensor_msgs/Image"
  std::string mcap_message_encoding; // e.g. "protobuf", "ros1"
  std::string mcap_schema_encoding;  // e.g. "protobuf", "ros1msg"

  // Returns "serializer:name", e.g. "protobuf:sensor_msgs.Image"
  std::string SchemaId() const { return serializer + ":" + name; }
};
SchemaId() is the canonical wire identifier for a topic’s message type. It matches the serializer:CppType syntax used in the unit YAML type field (after C++ :: separators are applied). Subscribers use the SchemaId() to verify type compatibility and to hand off schema information to the MCAP recorder.

On-demand serialization

A key design goal of Basis is that serialization is lazy. When a publisher calls Publish(msg):
  • Inproc subscribers in the same process receive a std::shared_ptr<const T_MSG> directly — no serialization occurs.
  • Network subscribers trigger serialization once per packet, regardless of how many remote subscribers are listening.
  • The recorder receives the already-serialized bytes when they exist, or triggers serialization on demand.
This means publishing to a topic that has only inproc subscribers has zero serialization overhead.

Specifying the serializer in YAML

In a .unit.yaml file, every type field uses the serializer:CppType format:
inputs:
  /camera_left:
    type: rosmsg:sensor_msgs::Image       # ROS1 msg plugin, sensor_msgs::Image
  /vector_data:
    type: protobuf:StampedVectorData      # Protobuf plugin, ::StampedVectorData
  /raw_imu:
    type: raw:ImuSample                   # Raw plugin, ImuSample POD struct

outputs:
  /lidar_sync_event:
    type: protobuf:LidarSyncEvent
The code generator splits the type string on the first :, uses the prefix to select the #include for the serializer header, and applies .:: substitution on the type name for protobuf package paths.
/pose:
  type: protobuf:geometry.Pose
The generator includes <basis/plugins/serialization/protobuf.h> and uses geometry::Pose as the C++ type.

Writing a custom serializer

To add a new serializer:
1

Implement the Serializer interface

Create a class that implements GetSerializedSize, SerializeToSpan, and DeserializeFromSpan as static template methods. Define a SERIALIZER_ID string constant.
namespace my_project::serialization {

class FlatbuffersSerializer : public basis::core::serialization::Serializer {
public:
  static constexpr char SERIALIZER_ID[] = "flatbuffers";

  template <typename T_MSG>
  static size_t GetSerializedSize(const T_MSG& message) {
    // return message byte size
  }

  template <typename T_MSG>
  static bool SerializeToSpan(const T_MSG& message, std::span<std::byte> span) {
    // write message into span
    return true;
  }

  template <typename T_MSG>
  static std::unique_ptr<T_MSG> DeserializeFromSpan(std::span<const std::byte> bytes) {
    // parse from bytes
  }
};

} // namespace my_project::serialization
2

Add a SerializationHandler specialization

Specialize basis::SerializationHandler for your message family. The Enable parameter lets you use type traits to match an entire family.
// In the same header, after the serializer class definition:

template <typename T_MSG>
struct basis::SerializationHandler<T_MSG,
    std::enable_if_t<my_project::IsFlatbuffersTable<T_MSG>::value>> {
  using type = my_project::serialization::FlatbuffersSerializer;
};
3

Wrap in a SerializationPlugin (optional)

If you want runtime schema loading and message introspection (required for MCAP recording), wrap your serializer in AutoSerializationPlugin:
using FlatbuffersPlugin =
    basis::core::serialization::AutoSerializationPlugin<FlatbuffersSerializer>;
Register an instance of this plugin with the recorder at startup.
4

Use the YAML prefix

Reference your message types in .unit.yaml files using the SERIALIZER_ID as the prefix:
inputs:
  /my_topic:
    type: flatbuffers:my_project.MyTable
Make sure the generated unit links against a target that provides your serializer header.

Plugin summary

YAML prefixSerializer classActivates forMCAP encoding
protobuf:ProtobufSerializergoogle::protobuf::Message subclassesprotobuf / protobuf
rosmsg:RosmsgSerializerros::message_traits::IsMessage<T> typesros1 / ros1msg
raw:RawSerializerPOD structs (explicit use only)none

Next steps

Unit YAML reference

How to specify the serializer prefix in input and output type fields

Code generation

How the generator includes serializer headers and wires up deserialization helpers

Transport

How the transport layer invokes serialization for network delivery

Recording and replay

How MessageSchema and MessageTypeInfo are used when writing MCAP files

Build docs developers (and LLMs) love