Skip to content
C++
Domain Track
Difficulty 2/5

Serialization in C++

C++ serialization patterns — manual binary, nlohmann/json, Protocol Buffers, MessagePack, std::from_chars for fast parsing, and versioning strategies.

TL;DR

C++ has no built-in serialization. Choose based on: human-readable (JSON), schema-enforced (Protobuf/Flatbuffers), or fast binary (manual/MessagePack). Always version your binary formats.


Manual Binary Serialization

Zero-copy, maximum control:

cpp
#include <cstring>
#include <vector>

// Simple buffer writer/reader
class BinaryWriter {
    std::vector<std::byte> buf_;

public:
    template<typename T>
    void write(const T& v) {
        static_assert(std::is_trivially_copyable_v<T>);
        auto* ptr = reinterpret_cast<const std::byte*>(&v);
        buf_.insert(buf_.end(), ptr, ptr + sizeof(T));
    }

    void write_string(std::string_view s) {
        write(static_cast<uint32_t>(s.size()));
        auto* ptr = reinterpret_cast<const std::byte*>(s.data());
        buf_.insert(buf_.end(), ptr, ptr + s.size());
    }

    std::span<const std::byte> data() const { return buf_; }
};

class BinaryReader {
    const std::byte* ptr_;
    const std::byte* end_;

public:
    BinaryReader(std::span<const std::byte> data)
        : ptr_{data.data()}, end_{data.data() + data.size()} {}

    template<typename T>
    T read() {
        if (ptr_ + sizeof(T) > end_) throw std::out_of_range("read past end");
        T v;
        std::memcpy(&v, ptr_, sizeof(T));
        ptr_ += sizeof(T);
        return v;
    }

    std::string read_string() {
        auto len = read<uint32_t>();
        if (ptr_ + len > end_) throw std::out_of_range("string too long");
        std::string s{reinterpret_cast<const char*>(ptr_), len};
        ptr_ += len;
        return s;
    }
};

struct Packet {
    uint32_t id;
    float    value;
    std::string name;

    void serialize(BinaryWriter& w) const {
        w.write(id); w.write(value); w.write_string(name);
    }
    static Packet deserialize(BinaryReader& r) {
        return {r.read<uint32_t>(), r.read<float>(), r.read_string()};
    }
};

JSON with nlohmann/json

The most popular C++ JSON library:

cpp
#include <nlohmann/json.hpp>
using json = nlohmann::json;

// Build JSON
json j;
j["name"]    = "Alice";
j["age"]     = 30;
j["active"]  = true;
j["scores"]  = {95, 88, 92};
j["address"] = {{"city", "New York"}, {"zip", "10001"}};

std::string s = j.dump(2);  // pretty-print with indent=2

// Parse JSON
json parsed = json::parse(R"({"name": "Bob", "age": 25})");
std::string name = parsed["name"];
int age          = parsed.at("age");  // throws if missing

// Structured bindings
auto [name2, age2] = parsed.items();

// Custom serialization via to_json/from_json
struct Person { std::string name; int age; };

void to_json(json& j, const Person& p) {
    j = json{{"name", p.name}, {"age", p.age}};
}
void from_json(const json& j, Person& p) {
    j.at("name").get_to(p.name);
    j.at("age").get_to(p.age);
}

Person alice{"Alice", 30};
json jalice = alice;          // to_json called automatically
Person bob = json::parse(R"({"name":"Bob","age":25})").get<Person>();

Protocol Buffers (Protobuf)

Schema-enforced, strongly typed, efficient:

protobuf
// person.proto
syntax = "proto3";
message Person {
    string name = 1;
    int32  age  = 2;
    repeated string emails = 3;
}
cpp
// C++ generated code usage
#include "person.pb.h"

Person p;
p.set_name("Alice");
p.set_age(30);
p.add_emails("alice@example.com");

// Serialize to string
std::string bytes;
p.SerializeToString(&bytes);

// Deserialize
Person p2;
p2.ParseFromString(bytes);
std::println("{}", p2.name());  // Alice

MessagePack (Fast Binary JSON)

Binary, schema-less, compact:

cpp
#include <msgpack.hpp>

struct Config {
    std::string host;
    int         port;
    MSGPACK_DEFINE(host, port)  // macro generates serialize/deserialize
};

// Serialize
Config cfg{"localhost", 8080};
msgpack::sbuffer sbuf;
msgpack::pack(sbuf, cfg);

// Deserialize
auto handle = msgpack::unpack(sbuf.data(), sbuf.size());
auto obj = handle.get();
Config cfg2;
obj.convert(cfg2);

Fast Number Parsing with from_chars

For custom text protocols where speed matters:

cpp
#include <charconv>

// Parse a CSV line: "1.5,2.7,3.9\n"
std::vector<double> parse_doubles(std::string_view line) {
    std::vector<double> vals;
    auto p = line.data();
    auto end = line.data() + line.size();

    while (p < end) {
        double v;
        auto [next, ec] = std::from_chars(p, end, v);
        if (ec != std::errc{}) break;
        vals.push_back(v);
        p = next;
        if (p < end && (*p == ',' || *p == '\n')) ++p;
    }
    return vals;
}

Versioned Binary Format

cpp
struct FileHeader {
    uint32_t magic;    // 0x43504200 "CPB\0"
    uint16_t version;  // format version
    uint16_t flags;
    uint64_t data_size;
};
static_assert(sizeof(FileHeader) == 16);

class VersionedSerializer {
    static constexpr uint32_t kMagic   = 0x43504200;
    static constexpr uint16_t kVersion = 2;

public:
    void write(std::ostream& out, const MyData& data) {
        FileHeader hdr{kMagic, kVersion, 0, 0};
        // serialize data into buffer...
        std::vector<std::byte> payload = serialize_payload(data);
        hdr.data_size = payload.size();
        out.write(reinterpret_cast<const char*>(&hdr), sizeof(hdr));
        out.write(reinterpret_cast<const char*>(payload.data()), payload.size());
    }

    MyData read(std::istream& in) {
        FileHeader hdr;
        in.read(reinterpret_cast<char*>(&hdr), sizeof(hdr));
        if (hdr.magic != kMagic) throw std::runtime_error("bad magic");
        if (hdr.version > kVersion) throw std::runtime_error("version too new");

        std::vector<std::byte> payload(hdr.data_size);
        in.read(reinterpret_cast<char*>(payload.data()), hdr.data_size);

        if (hdr.version == 1) return deserialize_v1(payload);
        return deserialize_v2(payload);
    }
};

Library Comparison

LibraryFormatSchemaSpeedUse when
nlohmann/jsonJSONNoMediumHuman-readable, debugging
simdjsonJSONNoVery fastHigh-throughput JSON parsing
ProtobufBinaryRequiredFastCross-language, versioned APIs
FlatbuffersBinaryRequiredVery fastZero-copy access
MessagePackBinaryNoFastBinary JSON without schema
Manual binaryCustomIn codeMaximumEmbedded, protocols
cerealJSON/Binary/XMLIn codeGoodC++ only, easy integration