Getting Started

FlatMessage is a zero-copy, schema-less serialization library built for Rust, offering efficient and flexible data serialization.

It is designed to be fast, memory-efficient, and easy to use, making it suitable for a wide range of applications, from simple data storage to complex network protocols.

A schema-less library means that you don't need to define a schema before you can start serializing and deserializing data (all necessary information is stored in the data itself). This means that the output buffer is larger than the total size of the data, but you are more flexible in scenarios where your data changes often.

A zero-copy library means that you don't need to copy the data from the serialized buffer; you can use references and slices to access the data directly. This is useful for larger datasets where copying them would add a performance penalty.

Installation

This chapter will guide you through installing and setting up FlatMessage in your Rust project.

System Requirements

FlatMessage requires:

  • Rust: Version 1.70 or later (2021 edition)
  • Operating System: Cross-platform (Windows, macOS, Linux)
  • Architecture: Support for 32-bit and 64-bit systems

Adding FlatMessage to Your Project

The easiest way to add FlatMessage to your project is through Cargo. Add the following dependency to your Cargo.toml file:

[dependencies]
flat_message = "*"

Use it

To use FlatMessage, define a structure and derive it from FlatMessage like in the following example:

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage)]
struct TestMessage {
    // fields
}
}

Deriving FlatMessage

To make a struct serializable with FlatMessage, simply add #[derive(FlatMessage)]:

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage)]
struct Person {
    name: String,
    age: u32,
    active: bool,
}
}

This automatically generates the necessary code to implement the FlatMessage trait for your struct.

Structure-Level Configuration (Optional)

You can configure the entire structure using the #[flat_message_options(...)] attribute in the following way:

#[flat_message_options(option-1 : value-1, option-2 : value-2, ...)]

or

#[flat_message_options(option-1 = value-1, option-2 = value-2, ...)]

For example:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(store_name = false, version = 1, checksum = true)]
struct OptimizedStruct {
    data: Vec<u8>,
}
}

Available Structure Options (Optional)

OptionTypeDefaultDescription
store_namebooltrueWhether to store a hash of the structure name in the serialized data
versionu80Version number for this structure (1-255). Value 0 means that the structure is not versioned.
checksumboolfalseWhether to include CRC32 checksum for data integrity
validate_nameboolfalseWhether to validate structure name during deserialization (this implies that the store_name is also set to true)
validate_checksum"auto" or "always" or "never""auto"When to validate checksums
compatible_versionsstringnoneVersion compatibility specification
optimized_unchecked_codebooltrueWhether to generate optimized unchecked code for deserialization or not. If not set the code generated for deserialize_from_unchecked will be the same as the one for deserialize_from
validate"strict" or "fallback""strict"Whether to use the default value if the deserialization fails. This attribute can be overridden at the field level (by useing #[flat_message_item(validate = "...")]).

Remarks:

  • The store_name option does not store the actual structure name, but a hash of it. That hash is being used to check if the structure you are deserializing into is the same as the one you serialized. However, this is not always neccesary (especially when talking about versioning and compabibility). If this is not needed, you should set the store_name option to false to save some space on the serialized buffer.
  • You can read more about versioning and compabibility in the Versioning chapter.
  • The checksum option is set to false by default. If set to true, the CRC32 checksum of the serialized data is being calculated and stored in the serialized buffer. This is useful to ensure data integrity during deserialization (usually when you are sending data over a network). You can read more on how checksums and validation work in the Checksum and Validation chapter.

Field-Level Configuration

Individual fields can be configured using #[flat_message_item(...)] in the following way:

#[flat_message_item(option-1 : value-1, option-2 : value-2, ...)]

or

#[flat_message_item(option-1 = value-1, option-2 = value-2, ...)]

This is useful when you want to specify how a field should be serialized / deserialized. The following example shows how to use the #[flat_message_item(repr = u8, kind = enum)] attribute to serialize an enum as a u8 value.

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct Example {
    normal_field: u32,
    
    #[flat_message_item(repr = u8, kind = enum)]
    status: Status,
}

#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
enum Status {
    Active = 1,
    Inactive = 2,
}
}

Remarks:

  • You can read more on how enums are being serialized in the Enums chapter or the Flags chapter.

Field Options

OptionValuesDescription
repru8, i8, u16, i16, u32, i32, u64, i64Representation type
kindenum, flags, struct, variantMarks field as enum , flags, variant or a structure type
align1, 2, 4, 8, 16Alignment of the field (only for structures and variants)
ignore or skiptrue or false (default is false)Ignores the field during serialization and deserialization
mandatorytrue or false (default is true)Marks the field as mandatory (required) for deserialization
validatestrict or fallback (default is strict)Specifies how to handle deserialization errors. If set to strict, the deserialization will fail if the field is present in the serialized data but it is not valid. If set to fallback, the field will be defaulted to the default value of the type if it the field is present in the serialized data but it is not valid.
defaultstringDefault value for the field. If specified, and the field is not mandatory, the default value will be used if the field is not present in the serialized data.

Remarks:

  • Fields of type PhantomData<T> are automatically ignored during serialization:
    #![allow(unused)]
    fn main() {
    use std::marker::PhantomData;
    
    #[derive(FlatMessage)]
    struct GenericStruct<T> {
        data: u32,
        _phantom: PhantomData<T>,  // This field is ignored
    }
    }
  • You can use the ignore or skip option to ignore a field during serialization and deserialization. This is useful when you want to skip a field that is not part of the structure you are deserializing into. The fields have to implement the Default trait.
    #![allow(unused)]
    fn main() {
    use std::marker::PhantomData;
    
    #[derive(Default)]
    struct MyNonSerializableData {
        a: u8,
        b: u32,
    }
    #[derive(FlatMessage)]
    #[flat_message_options(store_name = false)]
    struct Data {
        x: u8,
        #[flat_message_item(ignore = true)]
        y: MyNonSerializableData,
    }
    }
  • Mandatory fields are required for deserialization. If a mandatory field is not present in the serialized data, the deserialization will fail. On the other hand, if a field is not mandatory, and it is not found in the serialized data or there are some issues trying to deserialize it, it will be defaulted to the default value of the type. This implies that the trait Default is implemented for that type.
  • By default, fields are mandatory, except for Option<T> fields that are marked with mandatory = false (unless you specify the attribute #[flat_message_item(mandatory = true)]).
  • The default option can be used to provide a default value for a field. This is useful when you want to provide a default value for a field that is not mandatory. The default value can be a string literal, a function call, or a macro call.
    #![allow(unused)]
    fn main() {
    #[derive(FlatMessage)]
    #[flat_message_options(store_name = false)]
    struct MyDataV1 {
        a: u8,
    }
    #[derive(FlatMessage)]
    #[flat_message_options(store_name = false)]
    struct MyDataV2 {
        a: u8,
        // if the field is not present in the serialized data, it will be defaulted to vec![1,2,3]
        #[flat_message_item(mandatory = false, default = "vec![1,2,3]")]
        b: Vec<u8>,
    }
    }

Generated Code

When you derive FlatMessage, the following methods are automatically implemented:

#![allow(unused)]
fn main() {
impl<'a> FlatMessage<'a> for YourStruct {
    fn serialize_to(&self, output: &mut Storage, config: Config) -> Result<(), Error>;
    fn deserialize_from(input: &'a Storage) -> Result<Self, Error>;
    unsafe fn deserialize_from_unchecked(input: &'a Storage) -> Result<Self, Error>;
}
}

Complete Example

use flat_message::*;

#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
enum Priority {
    Low = 1,
    Medium = 2,
    High = 3,
}

#[derive(FlatMessage, Debug, PartialEq)]
#[flat_message_options(version = 1, store_name = true, checksum = true)]
struct Task {
    title: String,
    description: Option<String>,
    completed: bool,
    
    #[flat_message_item(repr = u8, kind = enum)]
    priority: Priority,
    
    tags: Vec<String>,
    created: Timestamp,
    id: UniqueID,
}

fn main() {
    let task = Task {
        title: "Learn FlatMessage".to_string(),
        description: Some("Read the documentation".to_string()),
        completed: false,
        priority: Priority::High,
        tags: vec!["learning".to_string(), "rust".to_string()],
    };

    // Create a serialization storage buffer
    let mut storage = Storage::default();
    if let Err(e) = task.serialize_to(&mut storage, Config::default()) {
        panic!("Error serializing task: {}", e);
    }

    // print the buffer
    println!("Buffer: {:?}", storage.as_slice());

    // Deserialize from buffer
    match Task::deserialize_from(&storage) {
        Ok(restored_task) => {
            assert_eq!(task, restored_task);
            println!("Task serialized and deserialized successfully");
        }
        Err(e) => {
            panic!("Error deserializing task: {}", e);
        }
    }
}

Upon execution, the following output is being printed:

Buffer: [70, 76, 77, 1, 5, 0, 1, 12, 22, 82, 101, 97, 100, 32, 116, 104, 101, 
         32, 100, 111, 99, 117, 109, 101, 110, 116, 97, 116, 105, 111, 110, 113, 
         190, 208, 155, 3, 17, 76, 101, 97, 114, 110, 32, 70, 108, 97, 116, 77, 
         101, 115, 115, 97, 103, 101, 2, 8, 108, 101, 97, 114, 110, 105, 110, 103, 
         4, 114, 117, 115, 116, 0, 0, 0, 14, 59, 111, 52, 19, 227, 228, 148, 14, 
         181, 101, 152, 142, 235, 22, 244, 13, 129, 116, 244, 8, 31, 36, 54, 69, 
         108, 136, 72, 244, 245, 75, 146, 228]
Task serialized and deserialized successfully

Core Concepts

FlatMessage relies on a few core concepts / components that are used to build the serialization and deserialization logic.

Storage

Storage is FlatMessage's primary buffer type for holding serialized data. It provides efficient memory management optimized for serialization workloads.

#![allow(unused)]
fn main() {
use flat_message::*;

// Using Storage for serialization
let mut storage = Storage::default();
data.serialize_to(&mut storage, Config::default())?;
}

Storage advantages:

  • Memory alignment: Uses Vec<u128> internally for better alignment
  • Reduced allocations: More efficient memory growth patterns
  • Optimized for serialization: Designed specifically for FlatMessage workloads

Storage API

#![allow(unused)]
fn main() {
impl Storage {
    // Create from existing byte buffer 
    pub fn from_buffer(input: &[u8]) -> Storage;
    
    // Create with a given capacity filled with zeros
    pub fn with_capacity(capacity: usize) -> Storage;

    // Get current size
    pub fn len(&self) -> usize;
}
}

Storage also implements Default trait, which creates an empty storage (with no allocated memory).

Config

Config controls serialization behavior and constraints:

#![allow(unused)]
fn main() {
use flat_message::*;

// Default configuration
let config = Config::default();

// Custom configuration
let config = ConfigBuilder::new()
    .max_size(1024 * 1024)  // 1MB limit
    .build();
}

Configuration Options

You can use ConfigBuilder to create a Config instance and provide a set of options (on how the serialization should be performed).

OptionDefaultDescription
max_size16MBMaximum serialized size allowed (in bytes). If the serialized size exceeds this limit, an error is returned.

Using Config

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct Data {
    content: Vec<u8>,
}

let data = Data { content: vec![1, 2, 3] };
let mut storage = Storage::default();

// Use custom size limit
let config = ConfigBuilder::new()
    .max_size(1024)  // Only allow 1KB
    .build();

match data.serialize_to(&mut storage, config) {
    Ok(()) => println!("Serialization successful"),
    Err(Error::ExceedMaxSize((actual, max))) => {
        println!("Data too large: {} bytes (max: {})", actual, max);
    }
    Err(e) => println!("Other error: {}", e),
}
}

FlatMessage Trait

The FlatMessage trait defines the core serialization interface:

#![allow(unused)]
fn main() {
pub trait FlatMessage<'a> {
    // Serialize data to a Storage buffer
    fn serialize_to(&self, output: &mut Storage, config: Config) -> Result<(), Error>;
    
    // Deserialize data from a buffer (with validation)
    fn deserialize_from(input: &'a Storage) -> Result<Self, Error>
    where
        Self: Sized;
    
    // Deserialize without validation (faster, but unsafe)
    unsafe fn deserialize_from_unchecked(input: &'a Storage) -> Result<Self, Error>
    where
        Self: Sized;
}
}

Serialization

Serialization transforms your struct into a byte buffer:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct Point {
    x: f32,
    y: f32,
}

let point = Point { x: 1.0, y: 2.0 };
let mut storage = Storage::default();

// Serialize the point
point.serialize_to(&mut storage, Config::default())?;

// Access the raw bytes
let bytes = storage.as_slice();
println!("Serialized size: {} bytes", bytes.len());
}

Deserialization

Deserialization reconstructs your struct from bytes:

#![allow(unused)]
fn main() {
// Deserialize with validation (recommended)
let restored_point = Point::deserialize_from(&storage)?;

// Deserialize without validation (faster, but risky)
let restored_point = unsafe {
    Point::deserialize_from_unchecked(&storage)?
};
}

Zero-Copy Deserialization

FlatMessage's key feature is zero-copy deserialization - it doesn't copy data from the buffer when possible. This is however highly dependent on the data type you are deserializing. Some of them such as String or Vec<T> require allocation and copying of the data. Also, basic types such as u32, f32, bool etc. are not zero-copy types (but since they are small, the performance impact is negligible).

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct Message<'a> {
    title: &'a str,        // Points directly into buffer
    tags: &'a [u32],       // Points directly into buffer
    owned_data: String,    // This still requires allocation
}
}

Zero-Copy vs Allocation

TypeZero-CopyNotes
&str✅ YesPoints into original buffer
&[T]✅ YesPoints into original buffer
&[u8; N]✅ YesPoints into original buffer
String❌ NoRequires allocation and copy
Vec<T>❌ NoRequires allocation and copy
Option<&str>✅ YesWhen Some, points into buffer
Option<String>❌ NoWhen Some, requires allocation

Lifetime Management

Zero-copy deserialization means your deserialized struct borrows from the original buffer:

#![allow(unused)]
fn main() {
// We assume that the `get_serialized_data` function returns a `Storage` object that contains the serialized data.
fn correct_process_message() -> Result<(), Error> {
    let storage = get_serialized_data();
    
    // This works - storage lives long enough
    let message = Message::deserialize_from(&storage)?;
    println!("Title: {}", message.title);
    
    Ok(())
} // storage and message both dropped here

fn broken_process_message() -> Result<Message<'static>, Error> {
    let storage = get_serialized_data();
    let message = Message::deserialize_from(&storage)?;
    
    // This won't compile - message can't outlive storage
    // Ok(message) // ❌ Compilation error
    
    unreachable!()
}
}

Performance Characteristics

Understanding performance implications helps you make good design decisions:

Serialization Performance

  • Direct types (u32, f64, bool): Fastest, just memory copies
  • Strings: Fast, just memory copies (for both &str and String)
  • Vectors/Slices: Fast, just memory copies
  • Enums: Fast, just the underlying representation

Deserialization Performance

  • Zero-copy types: Fastest, just pointer adjustments
  • Owned types: Slower, requires allocation and copying
  • Validation: deserialize_from() validates data, deserialize_from_unchecked() skips validation

Memory Usage

Being a schemaless library, FlatMessage has to store information on the type of the data being serialized. This is done by storing a hash over then type name and the type size.

As a general rule, for a structure with n fields, the serialized size will:

  • 8 bytes for the header
  • 5 bytes x n for the fields (for each field, 5 bytes are used to store the field name and the field size)
  • x bytes the actual content of the fields (where x is the sum of the sizes of all the fields)

Additionally, the following information is stored (if enabled)

  • 4 bytes for the checksum (if enabled)
  • 4 bytes for the name of the structure (if enabled)
  • 8 bytes for an unique identifier (if enabled)
  • 8 bytes for the timestamp of the serialization (if enabled)

This means that for the following structure:

#![allow(unused)]

fn main() {
#[derive(FlatMessage)]
struct Point {
    x: u32,
    y: u32,
}
}

The serialized size will be:

  • 8 bytes for the header
  • 5 bytes x 2 for the fields (for each field, 5 bytes are used to store the field name and the field size)
  • 8 bytes for the actual content of the fields (both fields are u32, so 8 bytes each)
  • 4 bytes for the name of the structure (enabled by default)

So a total of 30 bytes for the serialized size. The size could be 4 bytes smaller if we add #[flat_message_options(store_name = false)] to the structure.

Remarks:

  • The increase of size is not linear, but rather it depends on the number of fields and their sizes. For example a structure with only one string field that holds 1000 characters will only add 13 ore characters on the serialized buffer (1013 bytes) witch is insignificant relative to the actual content of the field.

Best Practices

  1. Use Storage for serialization: Storage is the only supported serialization target, optimized for FlatMessage workloads
  2. Prefer zero-copy types: Use &str over String, &[T] over Vec<T> when possible
  3. Validate when needed: Use deserialize_from() for untrusted data, deserialize_from_unchecked() for performance-critical trusted data
  4. Set appropriate limits: Use Config to prevent excessive memory usage
  5. Manage lifetimes carefully: Ensure buffers live long enough for zero-copy data

Example: Complete Workflow

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage, Debug, PartialEq)]
struct NetworkMessage<'a> {
    session_id: u64,
    command: &'a str,      // Zero-copy
    payload: &'a [u8],     // Zero-copy  
}

fn network_example() -> Result<(), flat_message::Error> {
    // Create message
    let original_message = NetworkMessage {
        session_id: 12345,
        command: "GET_USER",
        payload: &[1, 2, 3, 4, 5],
    };

    // Serialize with size limit
    let mut storage = Storage::default();
    let config = ConfigBuilder::new()
        .max_size(1024)  // 1KB limit
        .build();
    
    original_message.serialize_to(&mut storage, config)?;
    
    // Send over network (simulation)
    let network_bytes = storage.as_slice().to_vec();
    
    // Receive and deserialize
    let received_storage = Storage::from_buffer(&network_bytes);
    let received_message = NetworkMessage::deserialize_from(&received_storage)?;
    
    assert_eq!(original_message, received_message);
    println!("Message transmitted successfully!");
    
    Ok(())
}
}

Error Handling

FlatMessage provides comprehensive error handling through the Error enum. Understanding these errors helps you build robust serialization code.

Complete Error Reference

The following table lists all FlatMessage error types with their causes, typical scenarios, and recommended handling strategies:

Error TypeParametersWhen It OccursTypical CauseRecovery Strategy
InvalidHeaderLength(usize)Buffer sizeBuffer smaller than minimum header (8 bytes)Truncated data, wrong formatCheck data source, validate input
InvalidMagic-Magic number doesn't match "FLM\x01"Wrong file format, corruptionVerify file type, check data source
InvalidSize((u32, u32))(actual, expected)Size in header doesn't match buffer sizePartial read, corruptionRe-read data, validate source
InvalidOffsetSize-Invalid offset size encoding in headerCorruption, unsupported formatCheck format version, validate data
InvalidSizeToStoreMetaData((u32, u32))(actual, expected)Buffer too small for metadataIncomplete data, corruptionVerify complete transmission
InvalidHash((u32, u32))(actual, expected)CRC32 hash mismatchData corruption, tamperingRe-transmit data, check integrity
InvalidSizeToStoreFieldsTable((u32, u32))(actual, expected)Buffer too small for field tableTruncated dataEnsure complete data transfer
IncompatibleVersion(u8)Version numberStructure version incompatibilityVersion mismatchMigrate data, update code
FieldIsMissing(u32)Field hashField in data not in struct definitionSchema evolution, wrong structCheck struct version, migrate
InvalidFieldOffset((u32, u32))(actual, max)Field offset out of boundsCorruption, format errorValidate data integrity
FailToDeserialize(u32)Field hashFailed to deserialize specific fieldType mismatch, corruptionCheck field compatibility
NameNotStored-Name validation requested but not in dataMissing metadataDisable validation or add metadata
UnmatchedName-Structure name doesn't match stored nameWrong struct typeUse correct struct, check data
ChecksumNotStored-Checksum validation requested but not in dataMissing checksumDisable validation or add checksum
InvalidChecksum((u32, u32))(actual, expected)Checksum mismatchData corruptionRe-transmit, validate source
ExceedMaxSize((u32, u32))(actual, max)Serialized size exceeds maximumData too large, wrong limitIncrease limit, reduce data size

Error Categories

Data Format Errors

  • InvalidHeaderLength, InvalidMagic, InvalidSize, InvalidOffsetSize
  • Cause: Malformed or corrupted data format
  • Recovery: Validate data source, check file integrity

Data Integrity Errors

  • InvalidHash, InvalidChecksum
  • Cause: Data corruption during storage or transmission
  • Recovery: Re-transmit data, use error correction

Structure Compatibility Errors

  • IncompatibleVersion, FieldIsMissing, UnmatchedName
  • Cause: Schema evolution, version mismatches
  • Recovery: Migrate data, update compatibility rules

Configuration Errors

  • NameNotStored, ChecksumNotStored, ExceedMaxSize
  • Cause: Mismatched configuration between serialization and deserialization
  • Recovery: Align configurations, adjust limits

Field-Level Errors

  • InvalidFieldOffset, FailToDeserialize
  • Cause: Field-specific corruption or type mismatches
  • Recovery: Validate individual fields, check type compatibility

Example

The following example shows how to handle errors in a simple way when deserializing a struct.

#![allow(unused)]
fn main() {
use flat_message::*;

// consider that serialized_data is a buffer of the serialized data
let storage = Storage::from_buffer(serialized_data);

// Message is a struct that implements FlatMessage
match Message::deserialize_from(&storage) {
    Ok(restored_message) => {
        println!("Message serialized and deserialized successfully");
    }
    Err(e) => {
        panic!("Error deserializing message: {}", e);
    }
}
}

Serialization Model

FlatMessage uses a field-based serialization model that stores data in a structured binary format with hash tables for fast field lookup. The key advantage is zero-copy deserialization - instead of reconstructing objects in memory, it provides direct references to data within the serialized buffer.

This approach enables instant deserialization with no memory allocation, making it ideal for high-performance scenarios where read speed and memory efficiency are critical.

Binary Format

OffsetNameTypeObservations
+0Magic(3 bytes)'FLM'
+3Format versionu8currently value 1
+4Number of fieldsu16Total number of fields (data members) in the structure
+6Structure versionOptionFor structures that have multiple version, this byte holds the current version of the structure
+7Serializarion flagsu88 bits that provide information on the data
+8DataThe actual data from the structure
+?Hash tableu32 * Number of fieldsA hash table for quick access to the data
+?Offset table? * Number of fieldsA table with indexes from where the data starts
+?Timestampu64Only if the TIMESTAMP flag was set
+?Unique IDu64Only if the UNIQUEID flag was set
+?Structure Name Hashu32Only if the MAKEHASH flag was set
+?Data checksumu32Only if CHECKSUM flag was set

Remarks:

  • The Magic field is used to identify the file format. It should always be 'FLM'.
  • The Structure version field is used to indicate the version of the structure. Version 0 means that the structure has no versioning.
  • The Serializarion flags field is a bitmask that provides information about the data. The following flags are currently supported:
    • TIMESTAMP: Indicates that the structure has a timestamp.
    • UNIQUEID: Indicates that the structure has a unique ID.
    • MAKEHASH: Indicates that the structure has a name hash.
    • CHECKSUM: Indicates that the structure has a checksum.
  • The first 2 bits from the Serializarion flags field are use for offset size (1, 2 or 4 bytes). Smaller structus usually use 1 byte offset (meaning that the endire data is less than 255 bytes), while larger structs use a 2 or 4 bytes offset.

Supported Data Types

FlatMessage supports a variety of data types for serialization in the following way:

  1. as a direct value:
    #![allow(unused)]
    fn main() {
    struct Name { value: T } 
    }
  2. as a slice of values:
    #![allow(unused)]
    fn main() {
    struct Name { value: &[T] } 
    }
  3. as a vector of values:
    #![allow(unused)]
    fn main() {
    struct Name { value: Vec<T> } 
    }
  4. as an Option:
    #![allow(unused)]
    fn main() {
    struct Name { 
        value: Option<T>,
        slice: Option<&[T]>,
        vector: Option<Vec<T>>
    } 
    }

where T is the data type.

Remarks:

  • The main difference between a slice and a vector is that a slice is a reference to an array of values, while a vector is an owned collection of values. Slices are more memory efficient, but vectors provide more flexibility in terms of resizing and ownership. You can use them interchangeably, meaning that you can serialize an object that has a vector field and deserialize it into a slice, or vice versa. FlatMessage will handle the conversion automatically.

Keep in mind that deserialization of a slice is a no-cost operation, while deserialization of a vector requires allocation and copying of the data, which may incur some performance overhead.

Basic Types

Data TypeObjectSliceVectorOption
Boolean values: boolYesYesYesYes
Integer value: u8, u16, u32, u128, i8, i16, i32, i128YesYesYesYes
Float values: f32, f64YesYesYesYes

Remarks:

  • for bool values, deserialization using deserialize_from will validate if the value is 0 or 1, and will return an error if the value is not valid. If you are certain that the value is valid, you can use deserialize_from_unchecked to skip the validation step. This will speed up the deserialization process, but it is your responsibility to ensure that the value is valid.

Example

  1. Direct values:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example {
        boolean_value: bool,
        integer_value: u32,
        float_value: f64,
    }
    }
  2. Slices of values:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example {
        boolean_values: &[bool],
        integer_values: &[u32],
        float_values: &[f64],
    }
    }
  3. Vectors of values:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example {
        boolean_values: Vec<bool>,
        integer_values: Vec<u32>,
        float_values: Vec<f64>,
    }
    }
  4. Option values:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example {
        boolean_value: Option<bool>,
        u32_vec: Option<Vec<u32>>,
        f32_slice: Option<&[f32]>,
    }
    }

Strings

Strings types are represented as UTF-8 encoded bytes.

Data TypeObjectSliceVectorOption
String referemce: &strYes-YesYes
String object: StringYes-YesYes

Remarks:

  • deserialization using deserialize_from will validate if the string is a correct UTF-8 buffer. If you are certain that format is valid, you can use deserialize_from_unchecked to skip the validation step. This will speed up the deserialization process, but it is your responsibility to ensure that the string is actually a valid UTF-8 format.
  • You can use String and &str interchangeably, meaning that you can serialize an object that has a String field and deserialize it into a &str, or vice versa. This usually speeds up the deserialization process, as &str is a reference to a string slice and does not require allocation and copying of the data, while String is an owned collection of characters that requires allocation and copying of the data.

Example

  1. Direct values:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example<'a> {
        string_value: String,
        str_value: &'a str,
    }
    }
  2. Vectors of strings or string slices:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example<'a> {
        string_values: Vec<String>,
        str_values: Vec<&'a str>,
    }
    }
  3. Using Option values:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example<'a> {
        string_value: Option<String>,
        str_value: Option<&'a str>,
    }
    }

IP Addresses

Data TypeObjectSliceVectorOption
IP v4: Ipv4Addr (std::net::Ipv4Addr)Yes--Yes
IP v6: Ipv6Addr (std::net::Ipv6Addr)Yes--Yes
IP enum: IpAddr (std::net::IpAddr)Yes--Yes

Remarks:

  • The serialization size for Ipv4Addr is 4 bytes, and for Ipv6Addr it is 16 bytes.
  • The serialization size for IpAddr is 5 bybtes (if it is an Ipv4Addr) or 17 bytes (if it is an Ipv6Addr).

Example

  1. Direct values:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    use std::net::{Ipv4Addr, Ipv6Addr, IpAddr};
    
    #[derive(FlatMessage)]
    struct Example {
        ipv4_address: Ipv4Addr,
        ipv6_address: Ipv6Addr,
        ip_address: IpAddr,
    }
    }
  2. Using Option values:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    use std::net::{Ipv4Addr, Ipv6Addr, IpAddr};
    
    #[derive(FlatMessage)]
    struct Example {
        ipv4_address: Option<Ipv4Addr>,
        ipv6_address: Option<Ipv6Addr>,
        ip_address: Option<IpAddr>,
    }
    }

Unique ID

Data TypeObjectSliceVectorOption
Unique identfier (UniqueID or flat_message::UniqueID)Yes---

Remarks:

  • A UniqueID is a 64-bit value that must be non-zero and is intended to appear only once in a struct. It serves as a unique identifier for a message. The following example will not compile as it uses two fields with the type UniqueID:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    // code will not compile (two ids)
    #[derive(FlatMessage)]
    struct Example {
        id1: UniqueID
        id2: UniqueID
    }
    }
  • The use of UniqueID is optional—you don't need to include it in your structure unless you want to store messages in a database or require a unique identifier.

  • Since UniqueID can be used only once per struct, its field name is not stored; it will be automatically mapped to any field with the same type.

Methods

The following methods are available for an UniqueID:

MethodPurpose
new()Creates a new UniqueID instance with a globally unique, non-zero 64-bit value. It uses an atomic counter (GLOBAL_ID) to ensure each call produces a distinct value.
with_value(value: u64)Creates a UniqueID from a manually provided 64-bit value. This method bypasses automatic generation and should be used only when you already have a valid ID.
value(&self)Returns the underlying 64-bit integer value of the UniqueID. Useful for reading or storing the ID in external systems (e.g., databases).

Example

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage)]
struct Example {
    name: String,
    grade: u32,
    id: UniqueID
}
}

Timestamp

Data TypeObjectSliceVectorOption
Timestamp (Timestamp or flat_message::Timestamp)Yes---

Remarks:

  • A Timestamp is a 64-bit value that represents time in milliseconds since the UNIX epoch (January 1, 1970, 00:00:00 UTC) and is intended to appear only once in a struct. The following example will not compile as it uses two fields with the type Timestamp:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    // code will not compile (two timestamps)
    #[derive(FlatMessage)]
    struct Example {
        created_at: Timestamp,
        updated_at: Timestamp
    }
    }
  • The use of Timestamp is optional—you don't need to include it in your structure unless you want to track timing information.

  • Since Timestamp can be used only once per struct, its field name is not stored; it will be automatically mapped to any field with the same type.

  • The timestamp value is stored as an unsigned 64-bit integer, providing a range that can represent dates far into the future.

  • When system time retrieval fails, the timestamp defaults to 0 (representing the UNIX epoch).

  • The Timestamp type implements common traits like Copy, Clone, Debug, Eq, PartialEq, Ord, and PartialOrd, making it suitable for comparisons and sorting operations.

Methods

The following methods are available for a Timestamp:

MethodPurpose
with_value(value: u64)Creates a new Timestamp instance with a manually provided value in milliseconds since the UNIX epoch. Use this when you have a pre-existing timestamp value.
now()Creates a new Timestamp with the current system time in milliseconds since the UNIX epoch. Returns a timestamp with value 0 if system time cannot be obtained.
from_system_time(time: SystemTime)Creates a new Timestamp from a std::time::SystemTime value. Returns a timestamp with value 0 if the conversion fails.
value(&self)Returns the underlying 64-bit integer value of the Timestamp in milliseconds since the UNIX epoch. Useful for storing or transmitting the timestamp value.

Example

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage)]
struct LogEntry {
    message: String,
    level: u8,
    created_at: Timestamp
}

// Usage examples
let entry = LogEntry {
    message: "Application started".to_string(),
    level: 1,
    created_at: Timestamp::now()
};

// Create with specific timestamp
let historical_entry = LogEntry {
    message: "System boot".to_string(),
    level: 0,
    created_at: Timestamp::with_value(1640995200000) // Jan 1, 2022 00:00:00 UTC
};
}

Enums

Enums with explicitly defined backing types are supported for serialization and deserialization.

Data TypeObjectSliceVectorOption
Custom enums with #[derive(FlatMessageEnum)] and #[repr(primitive)]YesYesYesYes

Supported representations:

  • u8, i8, u16, i16, u32, i32, u64, i64

Remarks:

  • Enums must derive FlatMessageEnum and have an explicit #[repr(...)] attribute specifying the underlying primitive type.
  • Enum variants can have explicit values assigned, or they will use the default incrementing values starting from 0.
  • When using enums in structs, you must specify both the representation and kind in the field attribute: #[flat_message_item(repr = u8, kind = enum)].
  • Enums can be marked as #[sealed] for stricter version compatibility. Sealed enums include all variant names and values in their hash, making them incompatible with any version that adds, removes, or modifies variants. Non-sealed enums only include the enum name in their hash, allowing forward compatibility when adding new variants.
  • Both sealed and non-sealed enums validate that deserialized values match known variants in the current enum definition. The difference is in version compatibility: non-sealed enums allow adding variants without breaking hash compatibility, while sealed enums will fail to deserialize if the enum definition has changed in any way.
  • Deserialization using deserialize_from validates enum variant values. If you are certain the data is valid, you can use deserialize_from_unchecked to skip validation and improve performance.

Example

  1. Basic enum usage:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
    #[repr(u8)]
    enum Color {
        Red = 1,
        Green = 10,
        Blue = 100,
    }
    
    #[derive(Debug, FlatMessage)]
    struct Example {
        #[flat_message_item(repr = u8, kind = enum)]
        color: Color,
    }
    }
  2. Enum slices:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
    #[repr(u16)]
    enum Priority {
        Low = 1,
        Medium = 100,
        High = 1000,
    }
    
    #[derive(Debug, FlatMessage)]
    struct Example<'a> {
        #[flat_message_item(repr = u16, kind = enum)]
        priorities: &'a [Priority],
    }
    }
  3. Enum vectors:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
    #[repr(u32)]
    enum Status {
        Pending = 1,
        Processing = 2,
        Complete = 3,
    }
    
    #[derive(Debug, FlatMessage)]
    struct Example {
        #[flat_message_item(repr = u32, kind = enum)]
        statuses: Vec<Status>,
    }
    }
  4. Sealed enums for strict validation:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
    #[repr(u8)]
    #[sealed]
    enum Protocol {
        Http = 1,
        Https = 2,
        Ftp = 3,
    }
    
    #[derive(Debug, FlatMessage)]
    struct Example {
        #[flat_message_item(repr = u8, kind = enum)]
        protocol: Protocol,
    }
    }
  5. Using Option with enums:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
    #[repr(i16)]
    enum Temperature {
        Freezing = -100,
        Cold = -10,
        Warm = 20,
        Hot = 40,
    }
    
    #[derive(Debug, FlatMessage)]
    struct Example {
        #[flat_message_item(repr = i16, kind = enum)]
        temperature: Option<Temperature>,
    }
    }

Version Compatibility

Enums support forward compatibility when adding new variants:

#![allow(unused)]
fn main() {
// Version 1
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
enum Color {
    Red = 1,
    Green = 10,
    Blue = 100,
}

// Version 2 - compatible with version 1 data
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
enum Color {
    Red = 1,
    Green = 10,
    Blue = 100,
    Yellow = 200,  // New variant added
}
}

Data serialized with version 1 can be successfully deserialized with version 2 for non-sealed enums, as long as the specific variant values used in the serialized data exist in both versions. Sealed enums will fail to deserialize if any variants have been added, removed, or modified between versions.

Flags

Flags (bit fields) with explicitly defined backing types are supported for serialization and deserialization.

Data TypeObjectSliceVectorOption
Custom flags with #[derive(FlatMessageFlags)] and #[repr(transparent)]YesYesYesYes

Supported representations:

  • u8, u16, u32, u64, u128

Remarks:

  • Flags must derive FlatMessageFlags and have an explicit #[repr(transparent)] attribute.
  • Flags must declare available flag names in the #[flags(...)] attribute.
  • Individual flag values are defined using the add_flag! macro or as public constants.
  • When using flags in structs, you must specify both the representation and kind in the field attribute: #[flat_message_item(repr = u8, kind = flags)].
  • Flags can be marked as #[sealed] for stricter version compatibility. Sealed flags include all flag names and values in their hash, making them incompatible with any version that adds, removes, or modifies flags. Non-sealed flags only include the flag struct name in their hash, allowing forward compatibility when adding new flags.
  • Both sealed and non-sealed flags validate that deserialized values contain only valid flag combinations. If invalid flags are detected during deserialization, an error is returned.
  • Deserialization using deserialize_from validates flag values. If you are certain the data is valid, you can use deserialize_from_unchecked to skip validation and improve performance.

Available Methods

Flags automatically implement the FlagsSupport trait, providing the following methods:

  • from_value(value: T) - Creates flags from raw value, validates against known flags
  • to_value(&self) - Returns the raw underlying value
  • any_set(&self, flag: Self) - Checks if any of the specified flags are set
  • all_set(&self, flag: Self) - Checks if all of the specified flags are set
  • is_empty(&self) - Checks if no flags are set
  • set(&mut self, flag: Self) - Sets the specified flags
  • unset(&mut self, flag: Self) - Unsets the specified flags
  • toggle(&mut self, flag: Self) - Toggles the specified flags
  • clear(&mut self) - Clears all flags

Flags also support standard bitwise operations: | (OR), & (AND), ^ (XOR), and their assignment variants (|=, &=, ^=).

Example

  1. Basic flags usage:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug)]
    #[repr(transparent)]
    #[flags(Read, Write, Execute)]
    struct Permissions(u8);
    
    impl Permissions {
        add_flag!(Read = 1);
        add_flag!(Write = 2);
        add_flag!(Execute = 4);
    }
    
    #[derive(Debug, FlatMessage)]
    struct FileInfo {
        #[flat_message_item(repr = u8, kind = flags)]
        permissions: Permissions,
    }
    }
  2. Using flags with bit operations:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug)]
    #[repr(transparent)]
    #[flags(A, B, C)]
    struct Features(u32);
    
    impl Features {
        add_flag!(A = 1);
        add_flag!(B = 2);
        add_flag!(C = 4);
    }
    
    // Combining flags
    let mut features = Features::A | Features::B;
    
    // Checking flags
    assert!(features.all_set(Features::A));
    assert!(features.any_set(Features::A | Features::C));
    
    // Modifying flags
    features.set(Features::C);
    features.unset(Features::A);
    assert!(features.all_set(Features::B | Features::C));
    }
  3. Flags slices:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug)]
    #[repr(transparent)]
    #[flags(Low, Medium, High)]
    struct Priority(u16);
    
    impl Priority {
        add_flag!(Low = 1);
        add_flag!(Medium = 2);
        add_flag!(High = 4);
    }
    
    #[derive(Debug, FlatMessage)]
    struct TaskList<'a> {
        #[flat_message_item(repr = u16, kind = flags)]
        priorities: &'a [Priority],
    }
    }
  4. Flags vectors:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug)]
    #[repr(transparent)]
    #[flags(Debug, Info, Warning, Error)]
    struct LogLevel(u32);
    
    impl LogLevel {
        add_flag!(Debug = 1);
        add_flag!(Info = 2);
        add_flag!(Warning = 4);
        add_flag!(Error = 8);
    }
    
    #[derive(Debug, FlatMessage)]
    struct LogConfig {
        #[flat_message_item(repr = u32, kind = flags)]
        enabled_levels: Vec<LogLevel>,
    }
    }
  5. Sealed flags for strict validation:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug)]
    #[repr(transparent)]
    #[sealed]
    #[flags(Http, Https, Ftp)]
    struct Protocol(u8);
    
    impl Protocol {
        add_flag!(Http = 1);
        add_flag!(Https = 2);
        add_flag!(Ftp = 4);
    }
    
    #[derive(Debug, FlatMessage)]
    struct Connection {
        #[flat_message_item(repr = u8, kind = flags)]
        supported_protocols: Protocol,
    }
    }
  6. Using Option with flags:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug)]
    #[repr(transparent)]
    #[flags(Cache, Compress, Encrypt)]
    struct Options(u64);
    
    impl Options {
        add_flag!(Cache = 1);
        add_flag!(Compress = 2);
        add_flag!(Encrypt = 4);
    }
    
    #[derive(Debug, FlatMessage)]
    struct RequestConfig {
        #[flat_message_item(repr = u64, kind = flags)]
        options: Option<Options>,
    }
    }

Version Compatibility

Flags support forward compatibility when adding new flags:

#![allow(unused)]
fn main() {
// Version 1
#[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug)]
#[repr(transparent)]
#[flags(Read, Write)]
struct Permissions(u8);

impl Permissions {
    add_flag!(Read = 1);
    add_flag!(Write = 2);
}

// Version 2 - compatible with version 1 data
#[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug)]
#[repr(transparent)]
#[flags(Read, Write, Execute)]
struct Permissions(u8);

impl Permissions {
    add_flag!(Read = 1);
    add_flag!(Write = 2);
    add_flag!(Execute = 4);  // New flag added
}
}

Data serialized with version 1 can be successfully deserialized with version 2 for non-sealed flags, as long as the specific flag values used in the serialized data are valid in both versions. However, if version 2 data contains new flags (like Execute) and is deserialized with version 1, it will fail validation since version 1 doesn't recognize the new flag.

Sealed flags will fail to deserialize if any flags have been added, removed, or modified between versions, providing stricter version control at the cost of forward compatibility.

Fixed Size Buffer

Fixed-size byte arrays with compile-time known length are supported for serialization and deserialization.

Data TypeObjectSliceVectorOption
Fixed-size byte array: [u8; N]YesYesYesYes
Reference to fixed-size byte array: &[u8; N]Yes--Yes

Remarks:

  • Fixed-size byte arrays ([u8; N]) have their size known at compile time, where N is a constant value.
  • References to fixed-size arrays (&[u8; N]) can be used for zero-copy deserialization, avoiding memory allocation.
  • Deserialization validates that the stored size matches the expected compile-time size N. If the sizes don't match, deserialization will fail.
  • You can use deserialize_from_unchecked to skip size validation if you are certain the data is valid, which improves performance.

Example

  1. Direct fixed-size arrays:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example {
        buffer_10: [u8; 10],
        buffer_4: [u8; 4],
        small_buffer: [u8; 2],
    }
    }
  2. References to fixed-size arrays:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example<'a> {
        buffer_ref: &'a [u8; 10],
    }
    }
  3. Slices of fixed-size arrays:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example<'a> {
        multiple_buffers: &'a [[u8; 3]],
    }
    }
  4. Vectors of fixed-size arrays:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example {
        buffer_collection: Vec<[u8; 8]>,
    }
    }
  5. Optional fixed-size arrays:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessage)]
    struct Example<'a> {
        optional_buffer: Option<[u8; 16]>,
        optional_buffer_ref: Option<&'a [u8; 12]>,
        optional_buffer_vec: Option<Vec<[u8; 4]>>,
    }
    }

Performance Considerations

  • Fixed-size arrays are highly efficient as their size is known at compile time
  • No dynamic memory allocation is needed for the arrays themselves
  • Use references (&[u8; N]) when possible for zero-copy deserialization
  • For collections of fixed arrays, the memory layout is contiguous can easily be readu using a slice (&[[u8; N]]) - meaning zero-copy deserialization.
  • These types of buffer are often used for hash values (such as sha256 or sha512)

Structures

Custom structs with explicit alignment are supported for serialization and deserialization.

Data TypeObjectSliceVectorOption
Custom structs with #[derive(FlatMessageStruct)]Yes--Yes

Supported alignments:

  • 4-byte alignment (default)
  • 8-byte alignment (if one of the fields requires 64 bits alignament - such as Vec)
  • 16-byte alignment (if one of the fields requires 128 bits alignament - such as Vec)

Remarks:

  • Structs must derive FlatMessageStruct to be used as nested structures within other FlatMessage types.
  • When using structs in other structs, you must specify the alignment in the field attribute: #[flat_message_item(align = 4, kind = struct)].
  • The alignment must match the struct's actual memory alignment requirements based on its fields.
  • Structs automatically determine their required alignment based on their largest field's alignment requirements.
  • This type of serialization does not support metadata fields like Timestamp and UniqueID. You can add them but they will ont be serialized and in deserialization phase they will be defaulted to 0.
  • Fields can be marked with #[flat_message_item(ignore = true)] to exclude them from serialization.

Example

  1. Basic struct usage:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct MyData {
        a: u8,
        b: u32,
        c: u16,
        d: String,
    }
    
    #[derive(Debug, FlatMessage)]
    #[flat_message_options(store_name = false)]
    struct Test {
        x: u8,
        #[flat_message_item(align = 4, kind = struct)]
        data: MyData,
        y: u8,
    }
    }
  2. Struct with 8-byte alignment (contains u64 vectors or slices):

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct MyData {
        a: u8,
        b: u32,
        c: u16,
        d: String,
        values: Vec<u64>,  // Requires 8-byte alignment
    }
    
    #[derive(Debug, FlatMessage)]
    #[flat_message_options(store_name = false)]
    struct Test {
        x: u8,
        #[flat_message_item(align = 8, kind = struct)]
        data: MyData,
        y: u8,
    }
    }
  3. Struct with 16-byte alignment (contains u128 vectors or slices):

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct MyData {
        a: u8,
        values: Vec<u128>,  // Requires 16-byte alignment
    }
    
    #[derive(Debug, FlatMessage)]
    #[flat_message_options(store_name = false)]
    struct Test {
        x: u8,
        #[flat_message_item(align = 16, kind = struct)]
        data: MyData,
        y: u8,
    }
    }
  4. Structs with metadata fields:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct EventData {
        a: u8,
        b: u32,
        timestamp: Timestamp,  // ignored - will be defaulted to 0
        unique_id: UniqueID,   // ignored - will be defaulted to 0
    }
    
    #[derive(Debug, FlatMessage)]
    #[flat_message_options(store_name = false)]
    struct Event {
        #[flat_message_item(align = 4, kind = struct)]
        data: EventData,
    }
    }
  5. Using Option with structs:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct Configuration {
        timeout: u32,
        retries: u8,
    }
    
    #[derive(Debug, FlatMessage)]
    struct Request {
        #[flat_message_item(align = 4, kind = struct)]
        config: Option<Configuration>,
    }
    }
  6. Structs with ignored fields:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct ProcessData {
        pid: u32,
        name: String,
        #[flat_message_item(ignore = true)]
        runtime_state: String,  // Not serialized
    }
    
    #[derive(Debug, FlatMessage)]
    struct ProcessList {
        #[flat_message_item(align = 4, kind = struct)]
        processes: Vec<ProcessData>,
    }
    }
  7. Nested structs:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct LevelTwo {
        a: bool,
        l: Vec<i8>,
    }
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct LevelOne {
        a: u8,
        b: u32,
        c: u16,
        d: String,
        #[flat_message_item(align = 4, kind = struct)]
        e: LevelTwo,
    }
    #[derive(FlatMessage, Debug, PartialEq, Eq)]
    #[flat_message_options(store_name = false)]
    struct Test {
        x: u8,
        #[flat_message_item(align = 4, kind = struct)]
        d: LevelOne,
        a: u8,
    }
    }

Serialization Behavior

When structs are serialized:

  1. Field Ordering: Fields are reordered during serialization based on their alignment requirements (largest alignment first) to optimize memory layout.

  2. Hash Table: Each struct maintains a hash table of its fields for efficient deserialization and version compatibility.

  3. Reference Table: Offset information for each field is stored to enable random access during deserialization.

Packed Structs

Packed structs are a high-performance, memory-efficient serialization format optimized for sequential data layouts and minimal overhead. Unlike regular structs, packed structs store data in a continuous memory layout without hash tables or field lookups.

Data TypeObjectSliceVectorOption
Custom structs with #[derive(FlatMessagePacked)]Yes--No

Supported alignments:

  • 1-byte alignment (fields requiring only byte alignment)
  • 2-byte alignment (fields requiring 16-bit alignment)
  • 4-byte alignment (fields requiring 32-bit alignment - default for most cases)
  • 8-byte alignment (fields requiring 64-bit alignment - such as Vec)
  • 16-byte alignment (fields requiring 128-bit alignment - such as Vec)

Key Characteristics:

  • High Performance: Sequential memory layout provides optimal cache performance
  • Low Overhead: No hash tables or field lookup structures
  • Compact Size: Minimal metadata, only structure hash for validation
  • Field Reordering: Fields are automatically reordered by alignment (largest first) for optimal packing
  • Version Compatibility: Uses structure hash for version validation

Restrictions:

  • No Option<T> types supported
  • No Timestamp or UniqueID metadata fields
  • All fields must be mandatory (no mandatory = false)
  • No default values on deserialization failure (no validate = fallback)
  • Maximum 65,535 fields per struct
  • Fields can be ignored with #[flat_message_item(ignore = true)]

Usage

Packed structs must derive FlatMessagePacked and are used with kind = packed in field attributes:

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessagePacked, Debug, PartialEq, Eq)]
struct MyPackedData {
    x: i32,
    y: u32,
    label: String,
}

#[derive(FlatMessage, Debug)]
struct Container {
    #[flat_message_item(kind = packed, align = 1)]
    data: MyPackedData,
    other_field: String,
}
}

Examples

1. Basic Packed Struct (1-byte alignment)

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
struct Point {
    x: i32,
    y: u32,
    label: String,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
struct Test {
    #[flat_message_item(kind = packed, align = 1)]
    point: Point,
    description: String,
}

fn example() {
    let test_data = Test {
        point: Point {
            x: 10,
            y: 20,
            label: "Origin".to_string(),
        },
        description: "Test point".to_string(),
    };
    
    // Serialize and deserialize
    let mut storage = Storage::default();
    test_data.serialize_to(&mut storage, Config::default()).unwrap();
    let deserialized = Test::deserialize_from(&storage).unwrap();
    
    assert_eq!(test_data, deserialized);
}
}

2. Packed Struct with 4-byte Alignment

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
struct DataSet {
    data: Vec<u32>,
    index: u8,
}

#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
struct ComplexPoint {
    x: i8,
    y: i8,
    #[flat_message_item(kind = packed, align = 4)]
    dataset1: DataSet,
    #[flat_message_item(kind = packed, align = 4)]
    dataset2: DataSet,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
struct Container {
    #[flat_message_item(kind = packed, align = 4)]
    point: ComplexPoint,
    name: String,
}

fn example() {
    let container = Container {
        point: ComplexPoint {
            x: 10,
            y: 20,
            dataset1: DataSet {
                data: vec![1, 2, 3],
                index: 1,
            },
            dataset2: DataSet {
                data: vec![4, 5, 6],
                index: 2,
            },
        },
        name: "Complex data".to_string(),
    };
    
    // Efficient serialization and deserialization
    let mut storage = Storage::default();
    container.serialize_to(&mut storage, Config::default()).unwrap();
    let result = Container::deserialize_from(&storage).unwrap();
    
    assert_eq!(container, result);
}
}

3. Packed Struct with Ignored Fields

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
struct ProcessInfo {
    pid: u32,
    name: String,
    memory_usage: u64,
    #[flat_message_item(ignore = true)]
    runtime_data: String,  // Not serialized, will be default on deserialization
}

#[derive(Debug, FlatMessage)]
struct SystemSnapshot {
    #[flat_message_item(kind = packed, align = 8)]
    processes: Vec<ProcessInfo>,
    timestamp: u64,
}

fn example() {
    let snapshot = SystemSnapshot {
        processes: vec![
            ProcessInfo {
                pid: 1234,
                name: "my_app".to_string(),
                memory_usage: 1024 * 1024 * 50, // 50MB
                runtime_data: "This will be ignored".to_string(),
            },
        ],
        timestamp: 1640995200,
    };
    
    let mut storage = Storage::default();
    snapshot.serialize_to(&mut storage, Config::default()).unwrap();
    let result = SystemSnapshot::deserialize_from(&storage).unwrap();
    
    // runtime_data will be empty string (default) after deserialization
    assert_eq!(result.processes[0].runtime_data, String::default());
    assert_eq!(result.processes[0].pid, 1234);
}
}

Serialization Behavior

Packed structs use a fundamentally different serialization approach compared to regular structs:

1. Sequential Layout

Fields are stored sequentially in memory without indirection:

[Structure Hash][Field1][Padding if required][Field2][Padding if required][Field3]...

2. Automatic Field Reordering

Fields are automatically reordered by alignment requirements (largest alignment first) to minimize padding and optimize memory layout.

3. Minimal Metadata

  • Only a structure hash (4 bytes) is stored for version validation
  • No field hash tables or lookup structures
  • No field offset tables

4. Alignment-Based Padding

  • Padding is inserted between fields as needed for proper alignment
  • The struct's overall alignment is determined by its largest field alignment requirement
  • Padding follows standard C struct alignment rules

5. Structure Hash Validation

  • A FNV-32 hash of the structure definition is stored at the beginning
  • Hash includes field names, types, data formats, and alignment requirements
  • Deserialization fails if the hash doesn't match the expected structure

Performance Characteristics

AspectPacked StructsRegular StructsObservation
Serialization SpeedVery FastModerateSequential writes, no hash tables
Deserialization SpeedVery FastFastSequential reads, no field lookups
Memory OverheadMinimalModerateOnly 4-byte hash vs hash tables
Cache PerformanceExcellentGoodSequential access pattern
Version FlexibilityLimitedHighStrict hash matching required
Random Field AccessNot SupportedFastMust deserialize entire struct

When to Use Packed Structs

Choose packed structs when:

  • Performance is critical and you need maximum speed
  • Memory usage must be minimized
  • Data is accessed sequentially or entirely
  • Structure is stable and doesn't change frequently
  • You don't need optional fields or metadata

Choose regular structs when:

  • You need flexibility with optional fields
  • Structure evolves frequently
  • You need metadata fields (Timestamp, UniqueID)
  • Random field access is important
  • Backward/forward compatibility is required

Comparison with Regular Structs

FeaturePacked StructsRegular Structs
Derive macro#[derive(FlatMessagePacked)]#[derive(FlatMessageStruct)]
Field attributekind = packedkind = struct
Option types❌ Not supported✅ Supported
Metadata fields❌ Not supported❌ Not supported
Ignored fields✅ Supported✅ Supported
Field reordering✅ Automatic by alignment✅ Automatic by alignment
Hash table❌ No (only structure hash)✅ Yes (field hash table)
OverheadMinimal (4 bytes)Moderate (hash tables)
Version compatibilityStrict hash matchHash-based field lookup
PerformanceExcellentGood

Variant Enums

Variant enums (also known as algebraic data types or tagged unions) are supported for serialization and deserialization. They allow you to define an enum where each variant can contain different types of data.

Data TypeObjectSliceVectorOption
Custom variant enums with #[derive(FlatMessageVariant)]Yes--Yes

Supported variant types:

  • Basic types: u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, f32, f64, bool
  • Strings: String, &str
  • Collections: Vec<T>, &[T] (where T is a supported type)
  • Custom enums with #[derive(FlatMessageEnum)]
  • Flags with #[derive(FlatMessageFlags)]
  • Structs with #[derive(FlatMessageStruct)]
  • Nested variant enums
  • Unit variants (variants without data)
  • Options: Option<T> for any supported type T

Memory alignment:

  • Variant enums automatically determine their alignment based on the largest alignment requirement of their variants
  • Supported alignments: 1, 2, 4, 8, 16 bytes
  • When using variant enums in structs, you need to specify the alignment: #[flat_message_item(kind = variant, align = N)]

Remarks:

  • Variant enums must derive FlatMessageVariant
  • Each variant can contain at most one field (tuple-style variants)
  • Named struct-style variants are not supported
  • Unit variants (no associated data) are supported
  • Variant enums can be marked as #[sealed] for stricter version compatibility
  • Sealed variant enums include all variant names and types in their hash, making them incompatible with any version that adds, removes, or modifies variants
  • Non-sealed variant enums only include the enum name in their hash, allowing forward compatibility when adding new variants
  • When using complex types (enums, flags, structs) within variants, you must specify additional attributes: #[flat_message_item(kind = enum/flags/struct, repr = type, align = N)]
  • Deserialization using deserialize_from validates variant types and data. If you are certain the data is valid, you can use deserialize_from_unchecked to skip validation and improve performance

Examples

  1. Basic variant enum with primitive types:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    enum Value {
        Integer(i32),
        Float(f64),
        Text(String),
        Flag(bool),
    }
    
    #[derive(Debug, FlatMessage)]
    struct Message {
        #[flat_message_item(kind = variant, align = 8)]
        data: Value,
    }
    }
  2. Variant enum with collections:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    enum Container {
        Numbers(Vec<i32>),
        Words(Vec<String>),
        Bytes(Vec<u8>),
        Empty,  // Unit variant
    }
    
    #[derive(Debug, FlatMessage)]
    struct Document {
        #[flat_message_item(kind = variant, align = 4)]
        content: Container,
    }
    }
  3. Variant enum with custom enums:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
    #[repr(u8)]
    enum Priority {
        Low = 1,
        Medium = 2,
        High = 3,
    }
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    enum TaskData {
        Text(String),
        #[flat_message_item(kind = enum, repr = u8)]
        PriorityLevel(Priority),
        Urgent,  // Unit variant for urgent tasks
    }
    
    #[derive(Debug, FlatMessage)]
    struct Task {
        id: u32,
        #[flat_message_item(kind = variant, align = 1)]
        data: TaskData,
    }
    }
  4. Variant enum with flags:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(Copy, Clone, FlatMessageFlags, Eq, PartialEq, Debug)]
    #[repr(transparent)]
    #[flags(Read, Write, Execute)]
    struct Permissions(u8);
    impl Permissions {
        add_flag!(Read = 1);
        add_flag!(Write = 2);
        add_flag!(Execute = 4);
    }
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    enum FileInfo {
        Name(String),
        Size(u64),
        #[flat_message_item(kind = flags, repr = u8)]
        Access(Permissions),
    }
    
    #[derive(Debug, FlatMessage)]
    struct FileEntry {
        #[flat_message_item(kind = variant, align = 8)]
        info: FileInfo,
    }
    }
  5. Variant enum with structs:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct Point {
        x: f32,
        y: f32,
    }
    
    #[derive(FlatMessageStruct, Debug, PartialEq, Eq)]
    struct Color {
        r: u8,
        g: u8,
        b: u8,
    }
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    enum ShapeData {
        #[flat_message_item(kind = struct, align = 4)]
        Position(Point),
        #[flat_message_item(kind = struct, align = 1)]
        Appearance(Color),
        Label(String),
    }
    
    #[derive(Debug, FlatMessage)]
    struct Shape {
        #[flat_message_item(kind = variant, align = 4)]
        data: ShapeData,
    }
    }
  6. Variant enum with Option types:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    enum OptionalData {
        Text(Option<String>),
        Number(Option<i32>),
        Present,
        Absent,
    }
    
    #[derive(Debug, FlatMessage)]
    struct Record {
        #[flat_message_item(kind = variant, align = 4)]
        data: OptionalData,
    }
    }
  7. Sealed variant enum for strict validation:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    #[sealed]
    enum Protocol {
        Http(String),
        Https(String),
        Ftp(String),
        Unknown,
    }
    
    #[derive(Debug, FlatMessage)]
    struct Connection {
        #[flat_message_item(kind = variant, align = 1)]
        protocol: Protocol,
    }
    }
  8. Using Option with variant enums:

    #![allow(unused)]
    fn main() {
    use flat_message::*;
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    enum Response {
        Success(String),
        Error(u32),
        Timeout,
    }
    
    #[derive(Debug, FlatMessage)]
    struct ApiCall {
        endpoint: String,
        #[flat_message_item(kind = variant, align = 4)]
        response: Option<Response>,  // Optional response
    }
    }

Version Compatibility

Variant enums support forward compatibility when adding new variants:

#![allow(unused)]
fn main() {
// Version 1
#[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
enum Message {
    Text(String),
    Number(i32),
}

// Version 2 - compatible with version 1 data
#[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
enum Message {
    Text(String),
    Number(i32),
    Binary(Vec<u8>),  // New variant added
    Timestamp(u64),   // Another new variant
}
}

Compatibility rules:

  • Non-sealed variants: Data serialized with version 1 can be successfully deserialized with version 2, as long as the specific variants used in the serialized data exist in both versions
  • Sealed variants: Will fail to deserialize if any variants have been added, removed, or modified between versions
  • Existing variants: Must maintain the same type signature and attributes across versions
  • New variants: Can be safely added to non-sealed variant enums without breaking compatibility

Breaking changes:

  • Removing variants
  • Changing the type of data in existing variants
  • Changing attributes (kind, repr, align) of existing variants
  • Making a non-sealed variant enum sealed

Serialization Compatibility

FlatMessage provides comprehensive compatibility features that allow your data structures to evolve over time while maintaining interoperability between different versions. This chapter covers the key mechanisms for ensuring your serialized data can be safely shared between applications using different versions of the same message structures.

Overview

When working with serialized data in production systems, you often need to handle scenarios where:

  • Newer applications need to read data serialized by older versions
  • Older applications need to read data serialized by newer versions
  • Different services are running different versions of your data structures
  • Data migration requires careful compatibility planning

FlatMessage addresses these challenges through several key features:

  • Version Control: Every FlatMessage structure can be assigned a version number and specify which versions it's compatible with. This provides explicit control over backward and forward compatibility.

  • Flexible Field Management: Fields can be marked as mandatory or optional, with support for default values when fields are missing. This enables safe addition and modification of structure fields.

  • Sealed vs Non-Sealed Types: Enums, flags, and variants can be sealed (strict compatibility) or non-sealed (forward compatible), giving you control over how these types evolve.

Versioning

FlatMessage provides explicit version control for your data structures through the version and compatible_versions attributes. This system allows you to evolve your data formats safely while maintaining precise control over compatibility. Understanding how versioning interacts with field requirements is crucial for designing robust, evolvable data structures.

Basic Version Declaration

Every FlatMessage structure can declare a version number using the version attribute:

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage)]
#[flat_message_options(version = 1)]
struct UserProfile {
    name: String,
    email: String,
}
}

Key Points:

  • Version numbers range from 1 to 255 (version 0 means no versioning)
  • The version is stored in the 8-byte header of serialized data
  • During deserialization, version compatibility is checked BEFORE field validation

Version Compatibility Control

Use compatible_versions to specify which data versions your structure can deserialize:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(version = 2, compatible_versions = "1,2")]
struct UserProfileV2 {
    name: String,
    email: String,
    age: u32,  // New field - but is it compatible with v1 data?
}
}

This configuration means:

  • The structure declares itself as version 2
  • It accepts data from versions 1 and 2
  • Attempting to deserialize version 3+ data will fail with Error::IncompatibleVersion

Critical: Version compatibility only controls which data versions are accepted - it does NOT automatically handle field differences.

Deserialization Process Order

Understanding the deserialization process is crucial:

#![allow(unused)]
fn main() {
fn deserialize_example() {
    // 1. Header validation (magic number, size checks)
    // 2. Version compatibility check
    if header.version not in compatible_versions {
        return Err(Error::IncompatibleVersion(header.version));
    }
    // 3. Field validation and deserialization
    for each mandatory field {
        if field not found in data {
            return Err(Error::FieldIsMissing(field_hash));
        }
    }
    // 4. Struct construction
}
}

Version Compatibility Syntax

The compatible_versions attribute supports flexible range expressions:

Specific Versions

#![allow(unused)]
fn main() {
#[flat_message_options(version = 3, compatible_versions = "1,2,3")]
// Accepts exactly versions 1, 2, and 3
}

Range Operators

#![allow(unused)]
fn main() {
#[flat_message_options(version = 5, compatible_versions = "<5")]
// Accepts versions 1, 2, 3, 4 (less than 5)

#[flat_message_options(version = 4, compatible_versions = "<=4")]
// Accepts versions 1, 2, 3, 4 (less than or equal to 4)
}

Interval Ranges

#![allow(unused)]
fn main() {
#[flat_message_options(version = 10, compatible_versions = "5-10")]
// or "5:10" or "5..10"
// Accepts versions 5, 6, 7, 8, 9, 10
}

Combined Expressions

#![allow(unused)]
fn main() {
#[flat_message_options(version = 8, compatible_versions = "1,3,5-8")]
// Accepts versions 1, 3, 5, 6, 7, 8
}

Real-World Version Evolution Examples

Scenario 1: Version Check Without Field Compatibility

#![allow(unused)]
fn main() {
mod v1 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 1, compatible_versions = "1")]
    struct Config {
        value: u8,
    }
}

mod v2 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 2, compatible_versions = "1,2")]
    struct Config {
        value: u8,
        value2: u16,  // New mandatory field
    }
}
}

Results:

  • v1 data → v2 struct: ❌ Fails with FieldIsMissing (value2 not in v1 data)
  • v2 data → v1 struct: ❌ Fails with IncompatibleVersion(2) (v1 only accepts version 1)
  • v2 data → v2 struct: ✅ Works

Key Insight: Version compatibility and field compatibility are separate concerns!

Scenario 2: Forward Compatible Versioning

#![allow(unused)]
fn main() {
mod v1 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 1)]  // No compatible_versions = accepts any version
    struct Config {
        value: u8,
    }
}

mod v2 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 2)]  // No compatible_versions = accepts any version
    struct Config {
        value: u8,
        value2: u16,  // New mandatory field
    }
}
}

Results:

  • v1 data → v2 struct: ❌ Fails with FieldIsMissing (value2 not in v1 data)
  • v2 data → v1 struct: ✅ Works (v1 ignores extra fields it doesn't need)

Scenario 3: Safe Evolution with Optional Fields

#![allow(unused)]
fn main() {
mod v1 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 1)]
    struct Config {
        value: u8,
    }
}

mod v2 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 2)]
    struct Config {
        value: u8,
        #[flat_message_item(mandatory = false, default = "3")]
        value2: u16,  // Optional field with default
    }
}
}

Results:

  • v1 data → v2 struct: ✅ Works (value2 = 3 default)
  • v2 data → v1 struct: ✅ Works (v1 ignores extra fields)

Scenario 4: Safe Evolution with Option Fields

#![allow(unused)]
fn main() {
mod v1 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 1)]
    struct Config {
        value: u8,
    }
}

mod v2 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 2)]
    struct Config {
        value: u8,
        value2: Option<u16>,  // Option<T> automatically optional
    }
}
}

Results:

  • v1 data → v2 struct: ✅ Works (value2 = None default)
  • v2 data → v1 struct: ✅ Works (v1 ignores extra fields)

Key Insight: Option<T> fields are automatically optional, making them ideal for safe version evolution.

Scenario 5: Explicit Mandatory Option

#![allow(unused)]
fn main() {
mod v2 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 2)]
    struct Config {
        value: u8,
        #[flat_message_item(mandatory = true)]
        value2: Option<u16>,  // Explicitly mandatory Option<T>
    }
}
}

Results:

  • v1 data → v2 struct: ❌ Fails with FieldIsMissing (explicit mandatory override)
  • v2 data → v1 struct: ✅ Works (v1 ignores extra fields)

Common Version Compatibility Patterns

Strict Version Matching

#![allow(unused)]
fn main() {
#[flat_message_options(version = 2, compatible_versions = "2")]
// Only accepts exactly version 2 data
}

Use case: When you need strict control and no backward compatibility.

Backward Compatibility

#![allow(unused)]
fn main() {
#[flat_message_options(version = 3, compatible_versions = "1-3")]
// Version 3 struct can read data from versions 1, 2, and 3
}

Use case: When newer code needs to read older data formats. Requires careful field design with optional fields for additions. Option<T> fields are automatically optional, making this easier.

Forward Compatibility

#![allow(unused)]
fn main() {
#[flat_message_options(version = 1, compatible_versions = "1-5")]
// Version 1 struct can read data up to version 5
}

Use case: When older code needs to read newer data formats. Only works if newer versions only add optional fields.

Version Windows

#![allow(unused)]
fn main() {
#[flat_message_options(version = 5, compatible_versions = "3-7")]
// Accepts versions 3, 4, 5, 6, 7 but not 1, 2, or 8+
}

Use case: When you want to support a sliding window of versions, dropping support for very old formats.

Version Introspection

You can check the version of serialized data without full deserialization:

#![allow(unused)]
fn main() {
use flat_message::*;

fn check_version(storage: &Storage) -> Result<(), Error> {
    let info = StructureInformation::try_from(storage)?;
    
    match info.version() {
        Some(version) => {
            println!("Data version: {}", version);
            
            // Make decisions based on version
            if version < 2 {
                println!("Legacy format detected");
            } else if version > 5 {
                println!("Future version - may need migration");
            }
        }
        None => println!("No version information available"),
    }
    
    Ok(())
}
}

Error Handling

Version-related errors provide specific information:

#![allow(unused)]
fn main() {
match Config::deserialize_from(&storage) {
    Err(Error::IncompatibleVersion(found_version)) => {
        eprintln!("Cannot read version {} data with this struct", found_version);
        // Could attempt migration or request data in supported format
    }
    Err(Error::FieldIsMissing(field_hash)) => {
        eprintln!("Missing required field (hash: 0x{:08X})", field_hash);
        // Field compatibility issue, not version issue
    }
    Err(Error::InvalidHeaderLength(_)) => {
        eprintln!("Corrupted or invalid data");
    }
    Ok(config) => {
        // Successfully deserialized
    }
}
}

Advanced Version Handling

For complex migration scenarios, implement version-aware deserialization:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(version = 3, compatible_versions = "1-3")]
struct Config {
    host: String,
    port: u16,
    
    #[flat_message_item(mandatory = false, default = "30")]
    timeout: u32,    // Optional for v1/v2 compatibility
    
    #[flat_message_item(mandatory = false, default = "3")]
    retries: u8,     // Optional for v1/v2 compatibility
}

impl Config {
    pub fn from_any_version(storage: &Storage) -> Result<Self, Error> {
        // Check version before attempting deserialization
        let info = StructureInformation::try_from(storage)?;
        
        match info.version() {
            Some(1) => {
                println!("Migrating from v1 format");
                // v1 data should work with optional fields
                Self::deserialize_from(storage)
            }
            Some(2) => {
                println!("Reading v2 format");
                Self::deserialize_from(storage)
            }
            Some(3) => {
                println!("Reading current v3 format");
                Self::deserialize_from(storage)
            }
            Some(version) if version > 3 => {
                Err(Error::IncompatibleVersion(version))
            }
            None => {
                println!("No version info - assuming v1");
                Self::deserialize_from(storage)
            }
            _ => unreachable!(),
        }
    }
}
}

Best Practices

  1. Version from the start: Always include version = 1 on new structures
  2. Plan compatibility strategy: Decide upfront whether you need backward, forward, or bidirectional compatibility
  3. Use optional fields for additions: New fields should be optional (mandatory = false) to maintain backward compatibility
  4. Test all compatibility scenarios: Include tests for all supported version combinations
  5. Understand the two-phase validation: Version check happens before field validation
  6. Document breaking changes: Clearly mark when mandatory fields are added
  7. Use version introspection: Check versions before deserialization in multi-version systems
  8. Plan deprecation cycles: Allow time for systems to upgrade before removing compatibility

Common Pitfalls

  1. Assuming version compatibility handles fields: compatible_versions only controls version acceptance, not field compatibility
  2. Adding mandatory fields to backward-compatible versions: This breaks compatibility even with version ranges
  3. Not understanding Option default behavior: Option<T> fields are automatically optional unless explicitly marked with mandatory = true
  4. Not testing field compatibility: Version compatibility tests must also verify field-level compatibility

Key Takeaway

This versioning system, combined with careful field management and the automatic optional behavior of Option<T> fields, provides the foundation for robust data evolution in FlatMessage. The automatic optional behavior of Option<T> fields makes version evolution much safer and easier - when adding new fields to existing structures, prefer Option<T> types as they automatically provide backward compatibility without requiring explicit mandatory = false attributes.

Mandatory Fields and Default Values

FlatMessage fields are mandatory by default, meaning they must be present in the serialized data during deserialization. However, you can mark fields as optional using the mandatory = false attribute and provide default values. Understanding this system is crucial for designing evolvable data structures.

Understanding Mandatory vs Optional Fields

Mandatory Fields (Default Behavior)

By default, all fields in a FlatMessage structure are mandatory:

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage)]
struct UserProfile {
    name: String,      // Mandatory
    email: String,     // Mandatory
    age: u32,          // Mandatory
}
}

What happens during deserialization:

  • FlatMessage searches for each mandatory field's hash in the serialized data
  • If any mandatory field is missing, deserialization fails with Error::FieldIsMissing(hash)
  • This happens regardless of version compatibility settings

Optional Fields

Use mandatory = false to make fields optional:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct UserProfile {
    name: String,      // Mandatory
    email: String,     // Mandatory
    
    #[flat_message_item(mandatory = false)]
    age: u32,          // Optional - defaults to 0 if missing
    
    #[flat_message_item(mandatory = false)]
    bio: String,       // Optional - defaults to "" if missing
}
}

What happens during deserialization:

  • FlatMessage searches for the optional field's hash in the serialized data
  • If found, the field is deserialized normally
  • If not found, the field uses its default value (Type::default() or custom default)
  • No error is thrown for missing optional fields

Default Value Behavior

Type Defaults

When mandatory = false is specified without a custom default, the field uses the type's Default::default() implementation:

TypeDefault ValueNotes
u8, u16, u32, u640Numeric types default to zero
i8, i16, i32, i640Signed types also default to zero
f32, f640.0Floating point defaults to zero
boolfalseBoolean defaults to false
StringString::new()Empty (non allocated) String object
&str""Empty string (lifetime permitting)
Vec<T>[]Empty vector
Option<T>NoneOption defaults to None

Custom Default Values

You can specify custom default values using the default attribute:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct ServerConfig {
    host: String,      // Mandatory
    
    #[flat_message_item(mandatory = false, default = "8080")]
    port: u16,         // Optional with custom default
    
    #[flat_message_item(mandatory = false, default = "30")]
    timeout: u32,      // Optional with custom default
    
    #[flat_message_item(mandatory = false, default = "\"production\"")]
    environment: String, // Optional with custom default (note quotes)
}
}

String Default Syntax:

  • For string literals, use double quotes: default = "\"production\""
  • The system expects a valid Rust expression that evaluates to the field's type

Advanced Default Values

You can use constants, expressions, or function calls:

#![allow(unused)]
fn main() {
const DEFAULT_TIMEOUT: u32 = 60;
const DEFAULT_RETRIES: u8 = 3;

#[derive(FlatMessage)]
struct ApiConfig {
    endpoint: String,  // Mandatory
    
    #[flat_message_item(mandatory = false, default = "DEFAULT_TIMEOUT")]
    timeout: u32,      // Uses constant
    
    #[flat_message_item(mandatory = false, default = "DEFAULT_RETRIES")]
    retries: u8,       // Uses constant
    
    #[flat_message_item(mandatory = false, default = "vec![8080, 8081, 8082]")]
    allowed_ports: Vec<u16>, // Uses expression
}
}

Important: Option Fields Are Optional by Default

Key Change: Option<T> fields are automatically treated as optional (mandatory = false) by default:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct Config {
    host: String,           // Mandatory
    port: Option<u16>,      // Automatically optional! Uses None if missing
    
    #[flat_message_item(mandatory = true)]
    timeout: Option<u32>,   // Explicitly mandatory - must be present in data
}
}

Behavior:

  • Option<T> fields without explicit attributes: Optional (use None if missing)
  • Option<T> fields with mandatory = true: Mandatory (cause Error::FieldIsMissing if not present)
  • Option<T> fields with mandatory = false: Optional (use None if missing - same as default)

This makes Option<T> fields naturally compatible for version evolution since they default to being optional.

Relationship with Versioning

Mandatory fields interact with versioning in specific ways:

Version Compatibility is Checked First

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(version = 2, compatible_versions = "1,2")]
struct Config {
    host: String,      // Mandatory
    port: u16,         // Mandatory
    timeout: u32,      // Mandatory - added in v2
}
}

Deserialization process:

  1. Version check: Is the data version in compatible_versions?
    • If not → Error::IncompatibleVersion(version)
  2. Field validation: Are all mandatory fields present?
    • If not → Error::FieldIsMissing(hash)

Adding Mandatory Fields Breaks Compatibility

#![allow(unused)]
fn main() {
// Version 1 data serialized with this structure
#[derive(FlatMessage)]
#[flat_message_options(version = 1)]
struct Config {
    host: String,
    port: u16,
}

// Version 2 structure tries to read v1 data
#[derive(FlatMessage)]
#[flat_message_options(version = 2, compatible_versions = "1,2")]
struct Config {
    host: String,
    port: u16,
    timeout: u32,      // New mandatory field
}
}

Result: Even though version compatibility allows reading v1 data, deserialization will fail with Error::FieldIsMissing because timeout is mandatory but not present in v1 data.

Adding Optional Fields Maintains Compatibility

#![allow(unused)]
fn main() {
// Version 2 structure can successfully read v1 data
#[derive(FlatMessage)]
#[flat_message_options(version = 2, compatible_versions = "1,2")]
struct Config {
    host: String,
    port: u16,
    
    #[flat_message_item(mandatory = false, default = "30")]
    timeout: u32,      // Optional field with default
}
}

Result: v1 data → v2 struct works (timeout = 30), v2 data → v1 struct works (ignores timeout)

Real-World Compatibility Scenarios

Based on the test scenarios, here are the actual compatibility behaviors:

Scenario 1: Adding Mandatory Fields

#![allow(unused)]
fn main() {
mod v1 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 1)]
    struct Config { value: u8 }
}

mod v2 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 2)]
    struct Config { 
        value: u8,
        value2: u16,  // New mandatory field
    }
}
}
  • ✅ v2 data → v1 struct: Works (v1 ignores extra field)
  • ❌ v1 data → v2 struct: Fails with FieldIsMissing (value2 not in v1 data)

Scenario 2: Adding Optional Fields

#![allow(unused)]
fn main() {
mod v2 {
    #[derive(FlatMessage)]
    #[flat_message_options(version = 2)]
    struct Config { 
        value: u8,
        #[flat_message_item(mandatory = false, default = "3")]
        value2: u16,  // Optional field
    }
}
}
  • ✅ v2 data → v1 struct: Works (v1 ignores extra field)
  • ✅ v1 data → v2 struct: Works (value2 = 3 default)

Scenario 3: Option Fields (Automatically Optional)

#![allow(unused)]
fn main() {
mod v2 {
    #[derive(FlatMessage)]
    struct Config { 
        value: u8,
        value2: Option<u16>,  // Automatically optional (new default behavior)
    }
}
}
  • ✅ v2 data → v1 struct: Works (v1 ignores extra field)
  • ✅ v1 data → v2 struct: Works (value2 = None default)

Scenario 4: Option with Explicit mandatory = true

#![allow(unused)]
fn main() {
mod v2 {
    #[derive(FlatMessage)]
    struct Config { 
        value: u8,
        #[flat_message_item(mandatory = true)]
        value2: Option<u16>,  // Explicitly mandatory Option
    }
}
}
  • ✅ v2 data → v1 struct: Works (v1 ignores extra field)
  • ❌ v1 data → v2 struct: Fails with FieldIsMissing (explicitly mandatory Option)

Best Practices

  1. Default to mandatory: Make fields mandatory unless you specifically need them optional
  2. Plan for evolution: New fields should be optional to maintain backward compatibility
  3. Leverage Option for new fields: Option<T> fields are automatically optional, making them ideal for version evolution
  4. Use meaningful defaults: Provide sensible default values that won't break application logic
  5. Test compatibility: Always test deserialization across supported version combinations
  6. Version carefully: Consider mandatory field additions as breaking changes

Understanding mandatory fields and the automatic optional behavior of Option<T> is essential for designing robust, evolvable FlatMessage structures that can grow over time while maintaining compatibility with existing data.

Field Value Validation

Field value validation in FlatMessage provides a powerful mechanism for handling deserialization failures gracefully, making it particularly useful for versioning scenarios. The validate attribute allows you to specify how the system should behave when a field cannot be deserialized successfully.

Validation Modes

FlatMessage supports two validation modes:

  • validate = strict (default): Deserialization fails immediately if any field cannot be deserialized
  • validate = fallback: Falls back to the field's default value if deserialization fails

Syntax

The validate attribute can be applied at two levels:

Structure Level

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(validate = fallback)]
struct MyStruct {
    // All fields will use fallback validation by default
}
}

Field Level

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct MyStruct {
    #[flat_message_item(validate = strict)]
    critical_field: u32,
    
    #[flat_message_item(validate = fallback)]
    optional_field: Color,
}
}

Remarks:: The structure level validate attribute can be overridden at the field level (by useing #[flat_message_item(validate = "...")]).

Common Use Cases

1. Enum Evolution with Backward Compatibility

When adding new variants to enums, validate = fallback allows older code to handle newer enum values gracefully:

#![allow(unused)]
fn main() {
// Version 1
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug, Default)]
#[repr(u8)]
pub enum Color {
    #[default]
    Red = 1,
    Green = 10,
    Blue = 100,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
pub struct TestStruct {
    pub value: u8,
    #[flat_message_item(repr = u8, kind = enum, validate = fallback)]
    pub color: Color,
}
}
#![allow(unused)]
fn main() {
// Version 2 - adds Yellow variant
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
pub enum Color {
    Red = 1,
    Green = 10,
    Blue = 100,
    Yellow = 200,  // New variant
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
pub struct TestStruct {
    pub value: u8,
    #[flat_message_item(repr = u8, kind = enum)]
    pub color: Color,
}
}

When v1 code tries to deserialize data containing Yellow (value 200), it will fallback to the default Red value instead of failing:

#![allow(unused)]
fn main() {
// v2 serializes data with Yellow
let d_v2 = v2::TestStruct { value: 1, color: v2::Color::Yellow };
d_v2.serialize_to(&mut storage, Config::default()).unwrap();

// v1 deserializes successfully with fallback to default
let d_v1 = v1::TestStruct::deserialize_from(&storage).unwrap();
assert_eq!(d_v1.value, 1);
assert_eq!(d_v1.color, v1::Color::Red); // Fallback to default
}

2. Flags Evolution with Backward Compatibility

Similar to enums, flags can evolve by adding new flag variants. Using validate = fallback allows older code to handle newer flag combinations gracefully:

#![allow(unused)]
fn main() {
// Version 1
#[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug, Default)]
#[repr(transparent)]
#[flags(A,B)]
pub struct Flags(u8);
impl Flags {
    add_flag!(A = 1);
    add_flag!(B = 2);
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
pub struct TestStruct {
    pub value: u8,
    #[flat_message_item(repr = u8, kind = flags, validate = fallback)]
    pub flags: Flags,
}
}
#![allow(unused)]
fn main() {
// Version 2 - adds C flag
#[derive(Copy, Clone, FlatMessageFlags, PartialEq, Eq, Debug, Default)]
#[repr(transparent)]
#[flags(A,B,C)]
pub struct Flags(u8);
impl Flags {
    add_flag!(A = 1);
    add_flag!(B = 2);
    add_flag!(C = 4);  // New flag
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
pub struct TestStruct {
    pub value: u8,
    #[flat_message_item(repr = u8, kind = flags)]
    pub flags: Flags,
}
}

When v1 code tries to deserialize data containing the new C flag, it will fallback to the default empty flags instead of failing:

#![allow(unused)]
fn main() {
// v2 serializes data with C flag (unknown to v1)
let d_v2 = v2::TestStruct { 
    value: 1, 
    flags: v2::Flags::C | v2::Flags::B 
};
d_v2.serialize_to(&mut storage, Config::default()).unwrap();

// v1 deserializes successfully with fallback to default
let d_v1 = v1::TestStruct::deserialize_from(&storage).unwrap();
assert_eq!(d_v1.value, 1);
assert!(d_v1.flags.is_empty()); // Fallback to default (empty flags)
}

3. Variant Evolution with Backward Compatibility

Variants (also known as tagged unions or sum types) can evolve by adding new variants. Using validate = fallback allows older code to handle newer variant types gracefully by falling back to a default variant:

#![allow(unused)]
fn main() {
// Version 1
#[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
pub enum DataVariant {
    Byte(u8),
    String(String),
}

impl Default for DataVariant {
    fn default() -> Self {
        DataVariant::Byte(0)
    }
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
pub struct TestStruct {
    pub value: u8,
    #[flat_message_item(kind = variant, align = 1, validate = fallback)]
    pub data: DataVariant,
}
}
#![allow(unused)]
fn main() {
// Version 2 - adds DWord variant
#[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
pub enum DataVariant {
    Byte(u8),
    String(String),
    DWord(u32),  // New variant
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
pub struct TestStruct {
    pub value: u8,
    #[flat_message_item(kind = variant, align = 1)]
    pub data: DataVariant,
}
}

When v1 code tries to deserialize data containing the new DWord variant, it will fallback to the default variant instead of failing:

#![allow(unused)]
fn main() {
// v2 serializes data with DWord variant (unknown to v1)
let d_v2 = v2::TestStruct { 
    value: 1, 
    data: v2::DataVariant::DWord(12345) 
};
d_v2.serialize_to(&mut storage, Config::default()).unwrap();

// v1 deserializes successfully with fallback to default
let d_v1 = v1::TestStruct::deserialize_from(&storage).unwrap();
assert_eq!(d_v1.value, 1);
assert_eq!(d_v1.data, v1::DataVariant::Byte(0)); // Fallback to default variant
}

Important Notes for Variants:

  • The variant type must implement the Default trait for fallback validation to work
  • Unlike enums, variants cannot use the #[default] attribute on individual variants due to their complex data structure
  • The Default implementation should return a sensible default variant with appropriate default values for its contained data

4. Graceful Schema Evolution

Fallback validation enables smooth transitions when field types change or become incompatible:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(validate = fallback)] // Structure-level fallback validation
struct Configuration {
    #[flat_message_item(default = 30)]
    timeout_seconds: u32,
    
    log_level: LogLevel, // Uses structure-level fallback validation
    
    #[flat_message_item(validate = strict)] // Override to strict for critical field
    database_url: String, // Critical field - must not fail
}
}

How Fallback Validation Works

When validate = fallback is specified:

  1. Normal Operation: If the field can be deserialized normally, it uses the stored value
  2. Fallback Trigger: If deserialization fails (e.g., enum variant not found, type mismatch), the system:
    • Uses the default value if specified in the attribute
    • Uses the type's Default::default() implementation
    • For Option<T> types, defaults to None

Best Practices

When to Use Strict Validation

  • Critical fields where data integrity is essential
  • When you need to detect and handle incompatibilities explicitly
  • Fields where a default value doesn't make semantic sense

When to Use Fallback Validation

  • Optional or non-critical fields
  • Enum/Flags/Variant fields that may evolve over time
  • During data migration periods
  • When maintaining backward compatibility is important

Combining with Other Attributes

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct FlexibleStruct {
    // Critical field - must be present and valid
    #[flat_message_item(mandatory = true, validate = strict)]
    user_id: u64,
    
    // Optional field with fallback and custom default
    #[flat_message_item(
        mandatory = false, 
        validate = fallback, 
        default = "guest"
    )]
    username: String,
    
    // Enum that can evolve safely
    #[flat_message_item(
        repr = u8, 
        kind = enum, 
        validate = fallback
    )]
    user_type: UserType,
}
}

Implementation Notes

  • The validate attribute affects only the deserialization process
  • Serialization is not affected by validation settings
  • Fallback validation requires the type to implement Default trait
  • For enums, the #[default] attribute must be specified on one variant
  • The validation behavior is determined at compile time through proc macros

Sealed vs Non-Sealed Types

FlatMessage supports the #[sealed] attribute for enums, flags, and variants, providing two different approaches to type evolution and compatibility. This attribute fundamentally changes how hash values are computed and affects compatibility behavior when types are modified.

Understanding the Sealed Attribute

The #[sealed] attribute controls whether a type's hash includes all its internal structure:

  • Non-sealed (default): Hash includes only the type name
  • Sealed: Hash includes type name + all internal structure (variants, flags, etc.)

This applies to three main derive macros:

  • FlatMessageEnum with #[sealed]
  • FlatMessageFlags with #[sealed]
  • FlatMessageVariant with #[sealed]

Sealed vs Non-Sealed Enums

Hash Calculation Difference

#![allow(unused)]
fn main() {
use flat_message::*;

// Non-sealed enum (default)
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
enum Status {
    Active = 1,
    Inactive = 2,
}
// Hash: Hash("Status") - only name matters

// Sealed enum
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
#[sealed]
enum Protocol {
    Http = 1,
    Https = 2,
}
// Hash: Hash("Protocol" + "Http" + "Https") - includes all variants
}

Compatibility Behavior

Non-sealed enums allow adding new variants:

#![allow(unused)]
fn main() {
// Version 1
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
enum Color {
    Red = 1,
    Green = 2,
    Blue = 3,
}

// Version 2 - Compatible with Version 1 data
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
enum Color {
    Red = 1,
    Green = 2,
    Blue = 3,
    Yellow = 4,    // New variant - still compatible
}
}

Sealed enums break compatibility when modified:

#![allow(unused)]
fn main() {
// Version 1
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
#[sealed]
enum SecurityLevel {
    Public = 1,
    Internal = 2,
    Confidential = 3,
}

// Version 2 - NOT compatible with Version 1
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
#[sealed]
enum SecurityLevel {
    Public = 1,
    Internal = 2,
    Confidential = 3,
    TopSecret = 4,  // Adding this changes the hash completely
}
}

Sealed vs Non-Sealed Flags

Flags behave similarly to enums regarding the sealed attribute:

#![allow(unused)]
fn main() {
// Non-sealed flags (default)
#[derive(Copy, Clone, FlatMessageFlags, Eq, PartialEq, Debug)]
#[repr(transparent)]
#[flags(Read, Write, Execute)]
pub struct Permissions(u8);
impl Permissions {
    add_flag!(Read = 1);
    add_flag!(Write = 2);
    add_flag!(Execute = 4);
}
// Hash: Hash("Permissions") - can add new flags later

// Sealed flags
#[derive(Copy, Clone, FlatMessageFlags, Eq, PartialEq, Debug)]
#[repr(transparent)]
#[sealed]
#[flags(Admin, User, Guest)]
pub struct UserType(u8);
impl UserType {
    add_flag!(Admin = 1);
    add_flag!(User = 2);
    add_flag!(Guest = 4);
}
// Hash: Hash("UserType" + "Admin" + "User" + "Guest") - strict
}

Flag Evolution Example

#![allow(unused)]
fn main() {
// Version 1: Non-sealed flags
#[derive(Copy, Clone, FlatMessageFlags, Eq, PartialEq, Debug)]
#[repr(transparent)]
#[flags(Debug, Info, Warning, Error)]
pub struct LogFlags(u16);
impl LogFlags {
    add_flag!(Debug = 1);
    add_flag!(Info = 2);
    add_flag!(Warning = 4);
    add_flag!(Error = 8);
}

// Version 2: Add new flag - compatible
#[derive(Copy, Clone, FlatMessageFlags, Eq, PartialEq, Debug)]
#[repr(transparent)]
#[flags(Debug, Info, Warning, Error, Fatal)]
pub struct LogFlags(u16);
impl LogFlags {
    add_flag!(Debug = 1);
    add_flag!(Info = 2);
    add_flag!(Warning = 4);
    add_flag!(Error = 8);
    add_flag!(Fatal = 16);  // New flag - works with old data
}
}

Sealed vs Non-Sealed Variants

Variants (sum types) also support the sealed attribute:

#![allow(unused)]
fn main() {
// Non-sealed variant (default)
#[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
enum ApiResponse {
    Success(String),
    Error(u32),
}
// Hash: Hash("ApiResponse") - can add new variants

// Sealed variant
#[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
#[sealed]
enum DatabaseCommand {
    Insert(String),
    Update(String),
    Delete(u32),
}
// Hash: Hash("DatabaseCommand" + "Insert" + "Update" + "Delete") - strict
}

Variant Evolution

#![allow(unused)]
fn main() {
// Version 1: Non-sealed variant
mod v1 {
    use flat_message::*;
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    pub enum NetworkEvent {
        Connect(String),
        Disconnect,
    }
}

// Version 2: Add new variant - compatible
mod v2 {
    use flat_message::*;
    
    #[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
    pub enum NetworkEvent {
        Connect(String),
        Disconnect,
        Timeout(u32),  // New variant - works with v1 data
    }
}
}

When to Use Sealed Types

Use Sealed Types When:

  1. Security-critical: Cryptographic algorithms, security levels
  2. Stable protocols: Fixed command sets that shouldn't change
  3. Data integrity: Any modification should break compatibility
  4. Exact matching required: Systems must have identical type definitions
#![allow(unused)]
fn main() {
// Good candidates for sealed
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u8)]
#[sealed]
enum CryptoAlgorithm {
    Aes256 = 1,
    ChaCha20 = 2,
    // These must not change - security critical
}

#[derive(Copy, Clone, FlatMessageFlags, Eq, PartialEq, Debug)]
#[repr(transparent)]
#[sealed]
#[flags(Create, Read, Update, Delete)]
pub struct CrudOperations(u8);
// CRUD is stable and complete
}

Use Non-Sealed Types When:

  1. Expected evolution: Types will grow over time
  2. Forward compatibility: Older code should handle newer types
  3. API evolution: Public interfaces that might expand
  4. Configuration options: Settings that may increase
#![allow(unused)]
fn main() {
// Good candidates for non-sealed
#[derive(Copy, Clone, FlatMessageEnum, PartialEq, Eq, Debug)]
#[repr(u16)]
enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    InternalError = 500,
    // Many more status codes might be added
}

#[derive(FlatMessageVariant, Debug, PartialEq, Eq)]
enum LogLevel {
    Debug,
    Info(String),
    Warning(String),
    Error(String),
    // Might add Critical, Trace, etc.
}
}

Changing Type

When evolving your data structures over time, you may need to change the type of a field. FlatMessage provides specific behavior and compatibility rules when field types change between versions. Understanding these rules is crucial for maintaining backward and forward compatibility.

Overview

FlatMessage identifies fields using a combination of:

  • Field name
  • Type information (including representation for complex types)

When you change a field's type, FlatMessage's behavior depends on whether the field can still be identified and matched between versions.

Field Identification Rules

Primitive Types

For primitive types (u8, u16, String, Option<T>, etc.), the type is part of the field's identity. Changing a primitive type means the field will not be found during deserialization.

Example:

#![allow(unused)]
fn main() {
// Version 1
struct Message {
    value: u8,  // Field identified as "value:u8"
}

// Version 2
struct Message {
    value: u16, // Field identified as "value:u16" - different from v1
}
}

Complex Types (Enums, Flags, Variants)

For complex types with repr attributes, FlatMessage uses the representation type for field identification:

Same Representation (Field Found):

#![allow(unused)]
fn main() {
// Version 1
#[derive(FlatMessageEnum)]
#[repr(u8)]
enum Color { Red, Green, Blue }

struct Message {
    #[flat_message_item(repr = u8, kind = enum)]
    color: Color,  // Field identified as "color:u8"
}

// Version 2
#[derive(FlatMessageEnum)]
#[repr(u8)]
enum Shade { Light, Dark }

struct Message {
    #[flat_message_item(repr = u8, kind = enum)]
    color: Shade,  // Field identified as "color:u8" - SAME as v1
}
}

Different Representation (Field Not Found):

#![allow(unused)]
fn main() {
// Version 1
#[derive(FlatMessageEnum)]
#[repr(u8)]
enum Color { Red, Green, Blue }

struct Message {
    #[flat_message_item(repr = u8, kind = enum)]
    color: Color,  // Field identified as "color:u8"
}

// Version 2
#[derive(FlatMessageEnum)]
#[repr(u16)]
enum Shade { Light, Dark }

struct Message {
    #[flat_message_item(repr = u16, kind = enum)]
    color: Shade,  // Field identified as "color:u16" - DIFFERENT from v1
}
}

Compatibility Behavior

The behavior when deserializing depends on two field attributes and whether the field is found:

When Field is Found (Same Name + Compatible Type)

mandatoryvalidateResultBehavior
true/falsestrictFAILType validation fails, deserialization error
true/falsefallbackSUCCESSType validation fails, uses default value

Key Insight: When a field is found but types don't match exactly, the mandatory attribute is irrelevant because the field exists. Only validate determines the outcome.

When Field is Not Found (Different Name or Incompatible Type)

mandatoryvalidateResultBehavior
truestrict/fallbackFAILRequired field missing, deserialization error
falsestrict/fallbackSUCCESSOptional field missing, uses default value

Key Insight: When a field is not found, the validate attribute is irrelevant because no validation occurs. Only mandatory determines the outcome.

Type Change Scenarios

Primitive Type Changes

All primitive type changes result in field not being found:

#![allow(unused)]
fn main() {
// v1: value: u8 → v2: value: u16
// v1: text: String → v2: text: u32  
// v1: data: Option<u8> → v2: data: Option<u16>
}

Outcome: Field not found → mandatory attribute determines success/failure

Complex Type Changes - Same Representation

#![allow(unused)]
fn main() {
// Enums with same repr
#[repr(u8)] enum Color → #[repr(u8)] enum Shade

// Flags with same repr  
#[repr(u8)] struct Permissions → #[repr(u8)] struct Rights

// Variants with same repr
#[repr(u8)] enum Status → #[repr(u8)] enum Mode
}

Outcome: Field found → validate attribute determines success/failure

Complex Type Changes - Different Representation

#![allow(unused)]
fn main() {
// Enums with different repr
#[repr(u8)] enum Color → #[repr(u16)] enum Shade

// Flags with different repr
#[repr(u8)] struct Permissions → #[repr(u16)] struct Rights

// Variants with different repr
#[repr(u8)] enum Status → #[repr(u16)] enum Mode
}

Outcome: Field not found → mandatory attribute determines success/failure

Default Values

When deserialization succeeds but uses fallback behavior, FlatMessage uses the default value for the target type:

  • Primitive types: Language defaults (0 for integers, "" for strings, None for options)
  • Enums: The variant marked with #[default]
  • Flags: Empty flags (no flags set)
  • Variants: The variant marked with #[default]

Best Practices

For Backward Compatibility

  1. Avoid changing primitive types - they always break compatibility
  2. Keep representation consistent for complex types when possible
  3. Use mandatory = false for fields that might be removed or changed
  4. Use validate = fallback to gracefully handle type mismatches

For Forward Compatibility

  1. Plan representation carefully - changing repr breaks compatibility
  2. Consider using Option<T> to make fields truly optional
  3. Document default values clearly for fallback scenarios

Migration Strategies

  1. Gradual migration: Introduce new field, deprecate old field
  2. Representation preservation: Keep same repr when changing complex types
  3. Fallback-friendly defaults: Ensure default values are meaningful for your application

Error Types

When type changes cause deserialization failures:

  • Error::FieldIsMissing: Field not found (different types or names)
  • Error::FailToDeserialize: Field found but type validation failed with strict validation

Nested Structures and Version Compatibility

When working with nested structures in FlatMessage, understanding how different struct types behave during version changes is crucial for maintaining compatibility between different versions of your application. This chapter explains the compatibility implications of using FlatMessageStruct and FlatMessagePacked types as nested structures.

Overview

FlatMessage supports two types of nested structures, each with different characteristics and compatibility behaviors:

  1. FlatMessageStruct - Hash-based structures with flexible compatibility
  2. FlatMessagePacked - Hash-validated structures with strict compatibility

The choice between these types significantly impacts how your data structures can evolve over time.

FlatMessageStruct - Hash-Based Structures

FlatMessageStruct uses a hash table approach for field storage and lookup, making it more flexible for version evolution.

Key Characteristics

  • Field Lookup: Uses hash tables for field identification and access
  • Memory Layout: Not sequential - fields can be accessed in any order
  • Metadata Support: Supports Timestamp and UniqueID metadata fields (though they are ignored during serialization in nested contexts)
  • Option Support: Can be wrapped in Option<T>
  • Validation: Supports field-level validation attributes
  • Compatibility: Can be compatible with different versions of the same struct (pending a proper usage of mandatory and validate attributes)

Basic Usage

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(Debug, PartialEq, Eq, FlatMessageStruct)]
struct UserProfile {
    pub name: String,
    pub age: u32,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
struct User {
    pub id: u8,
    #[flat_message_item(kind = struct, align = 4)]
    pub profile: UserProfile,
}
}

FlatMessagePacked - Hash-Validated Structures

FlatMessagePacked uses a sequential memory layout with hash validation for structure integrity.

Key Characteristics

  • Memory Layout: Sequential memory layout for optimal performance
  • Hash Validation: Uses structure hash for validation during deserialization
  • No Metadata: No support for Timestamp or UniqueID metadata fields
  • No Options: Cannot be wrapped in Option<T>
  • Field Ordering: Fields are automatically reordered by alignment for optimal packing
  • Compatibility: Strict - any structural change breaks compatibility

Basic Usage

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
struct Coordinates {
    pub x: f32,
    pub y: f32,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
struct Location {
    pub id: u8,
    #[flat_message_item(kind = packed, align = 4)]
    pub coords: Coordinates,
}
}

Version Compatibility Scenarios

The following scenarios demonstrate how different structural changes affect compatibility between versions. These are based on comprehensive test cases that verify the actual behavior.

Scenario 1: FlatMessageStruct - Adding Mandatory Fields with Strict Validation

Version 1:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessageStruct)]
pub struct NestedStruct {
    pub value: u32,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
pub struct Test {
    pub id: u8,
    #[flat_message_item(kind = struct, align = 4)]
    pub nested: NestedStruct,
}
}

Version 2:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessageStruct)]
pub struct NestedStruct {
    pub value: u32,
    pub new_field: u16, // New mandatory field added
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
pub struct Test {
    pub id: u8,
    #[flat_message_item(kind = struct, align = 4, validate = strict)]
    pub nested: NestedStruct,
}
}

Compatibility:

  • V1 → V2: ❌ FAILS - Missing mandatory field causes deserialization failure
  • V2 → V1: ✅ SUCCEEDS - Extra fields are ignored

Scenario 2: FlatMessageStruct - Adding Optional Fields

Version 2 (Modified):

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessageStruct)]
pub struct NestedStruct {
    pub value: u32,
    #[flat_message_item(mandatory = false, validate = fallback, default = 42)]
    pub new_field: u16, // New optional field with default
}
}

Compatibility:

  • V1 → V2: ✅ SUCCEEDS - Missing optional field uses default value (42)
  • V2 → V1: ✅ SUCCEEDS - Extra fields are ignored

Scenario 3: FlatMessageStruct - Fallback Validation on Parent Field

Version 2 (Modified):

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessage)]
pub struct Test {
    pub id: u8,
    #[flat_message_item(kind = struct, align = 4, validate = fallback)]
    pub nested: NestedStruct, // validate = fallback on the field itself
}

impl Default for NestedStruct {
    fn default() -> Self {
        Self { value: 0, new_field: 0 }
    }
}
}

Compatibility:

  • V1 → V2: ✅ SUCCEEDS - When struct deserialization fails, uses Default::default()
  • V2 → V1: ✅ SUCCEEDS - Extra fields are ignored

Scenario 4: FlatMessageStruct - Fallback Validation on Child Fields

Version 2 (Modified):

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessageStruct)]
pub struct NestedStruct {
    #[flat_message_item(validate = fallback)]
    pub value: u32,
    #[flat_message_item(validate = fallback)]
    pub new_field: u16, // validate = fallback on individual fields
}
}

Compatibility:

  • V1 → V2: ❌ FAILS - Field-level fallback doesn't help with missing mandatory fields
  • V2 → V1: ✅ SUCCEEDS - Extra fields are ignored

Key Insight: validate = fallback on individual struct fields only helps with field-level validation issues, not with missing mandatory fields at the struct level.

Scenario 5: FlatMessageStruct - Mandatory = False on Parent Field

Version 2 (Modified):

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessage)]
pub struct Test {
    pub id: u8,
    #[flat_message_item(kind = struct, align = 4, mandatory = false)]
    pub nested: NestedStruct, // mandatory = false on the field
}
}

Compatibility:

  • V1 → V2: ❌ FAILS - mandatory = false doesn't help if the struct content has structural changes
  • V2 → V1: ✅ SUCCEEDS - Extra fields are ignored

Key Insight: Setting mandatory = false on the parent field doesn't help when the nested struct itself has incompatible changes.

Scenario 6: FlatMessageStruct - Removing Fields

Version 1:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessageStruct)]
pub struct NestedStruct {
    pub value: u32,
    pub old_field: u16,
}
}

Version 2:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessageStruct)]
pub struct NestedStruct {
    pub value: u32, // old_field removed
}
}

Compatibility:

  • V1 → V2: ✅ SUCCEEDS - Extra fields in data are ignored
  • V2 → V1: ❌ FAILS - Missing mandatory field causes deserialization failure

Scenario 7: FlatMessagePacked - Adding Fields

Version 1:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
pub struct NestedStruct {
    pub value: u32,
}
}

Version 2:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
pub struct NestedStruct {
    pub value: u32,
    pub new_field: u16, // Any change breaks compatibility
}
}

Compatibility:

  • V1 → V2: ❌ FAILS - Hash validation detects structural change
  • V2 → V1: ❌ FAILS - Hash validation detects structural change

Key Insight: ANY structural change to a packed struct breaks compatibility due to hash validation.

Scenario 8: FlatMessagePacked - Fallback Validation on Parent Field

Version 2 (Modified):

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessage)]
pub struct Test {
    pub id: u8,
    #[flat_message_item(kind = packed, align = 1, validate = fallback)]
    pub nested: NestedStruct,
}

impl Default for NestedStruct {
    fn default() -> Self {
        Self { value: 999, new_field: 888 }
    }
}
}

Compatibility:

  • V1 → V2: ✅ SUCCEEDS - When packed struct validation fails, uses Default::default()
  • V2 → V1: ❌ FAILS - Hash validation still detects structural change

Scenario 9: FlatMessagePacked - Mandatory = False on Parent Field

Version 2 (Modified):

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessage)]
pub struct Test {
    pub id: u8,
    #[flat_message_item(kind = packed, align = 1, mandatory = false)]
    pub nested: NestedStruct,
}
}

Compatibility:

  • V1 → V2: ❌ FAILS - Hash validation fails before mandatory = false can take effect
  • V2 → V1: ❌ FAILS - Hash validation detects structural change

Scenario 10: FlatMessagePacked - Type Changes

Version 1:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
pub struct NestedStruct {
    pub value: u32,
    pub data: u8,
}
}

Version 2:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessagePacked)]
pub struct NestedStruct {
    pub value: u32,
    pub data: u16, // Type changed from u8 to u16
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
pub struct Test {
    pub id: u8,
    #[flat_message_item(kind = packed, align = 1, validate = fallback)]
    pub nested: NestedStruct,
}
}

Compatibility:

  • V1 → V2: ✅ SUCCEEDS - Hash validation fails, falls back to default
  • V2 → V1: ❌ FAILS - Hash validation detects structural change

Summary and Best Practices

FlatMessageStruct Compatibility Rules

Compatible Changes:

  • Adding optional fields (mandatory = false)
  • Removing fields (forward compatibility only)
  • Changing field order (fields are hash-based)

Incompatible Changes:

  • Adding mandatory fields without fallback strategies
  • Removing fields that newer versions still expect

FlatMessagePacked Compatibility Rules

Compatible Changes:

  • None - packed structs are designed for performance, not evolution

Incompatible Changes:

  • Adding any fields
  • Removing any fields
  • Changing field types
  • Changing field order (though order is automatically optimized)

Mitigation Strategies

For FlatMessageStruct

  1. Use Optional Fields: Mark new fields as mandatory = false

    #![allow(unused)]
    fn main() {
    #[flat_message_item(mandatory = false, default = 42)]
    pub new_field: u16,
    }
  2. Use Fallback Validation on Parent: Apply validate = fallback to the struct field

    #![allow(unused)]
    fn main() {
    #[flat_message_item(kind = struct, align = 4, validate = fallback)]
    pub nested: NestedStruct,
    }
  3. Implement Default Carefully: Ensure Default implementations make sense for your domain

    #![allow(unused)]
    fn main() {
    impl Default for NestedStruct {
        fn default() -> Self {
            Self { value: 0, new_field: 100 }
        }
    }
    }

For FlatMessagePacked

  1. Use Fallback Validation: Apply validate = fallback to the packed struct field

    #![allow(unused)]
    fn main() {
    #[flat_message_item(kind = packed, align = 4, validate = fallback)]
    pub packed_data: PackedStruct,
    }
  2. Design for Immutability: Treat packed structs as immutable once deployed

  3. Version at Container Level: Use versioning on the parent FlatMessage struct instead

When to Use Each Type

Choose FlatMessageStruct when:

  • You need version evolution capabilities
  • Field access patterns are random or sparse
  • You have optional/metadata fields
  • Schema flexibility is important

Choose FlatMessagePacked when:

  • Maximum performance is critical
  • Memory layout optimization is important
  • The structure is stable and won't change
  • You need the smallest possible serialized size

Advanced Features

While FlatMessage provides a lot of flexibility and power out of the box, there are some advanced features that you can use to get the most out of it, including:

  • how some types can be interchangeable
  • how to set default values
  • how to ignore fields
  • how to use checksums and validation

Type Interchangeability

One of FlatMessage's powerful features is the ability to use different but compatible types during serialization and deserialization. This flexibility enables efficient memory usage and performance optimization.

1. Vec vs Slice Interchangeability

You can serialize data with Vec<T> and deserialize it as &[T], or vice versa:

#![allow(unused)]
fn main() {
use flat_message::*;

// Serialization struct using Vec
#[derive(FlatMessage)]
struct DataWriter {
    numbers: Vec<u32>,
    names: Vec<String>,
}

// Deserialization struct using slices (zero-copy)
#[derive(FlatMessage)]
struct DataReader<'a> {
    numbers: &'a [u32],      // Zero-copy reference
    names: Vec<&'a str>,,    // Zero-copy reference for &str, allocation for Vec
}

fn example() -> Result<(), Error> {
    // Create data with Vec (ownership)
    let writer_data = DataWriter {
        numbers: vec![1, 2, 3, 4, 5],
        names: vec!["Alice".to_string(), "Bob".to_string()],
    };

    // Serialize
    let mut storage = Storage::default();
    writer_data.serialize_to(&mut storage, Config::default())?;

    // Deserialize as slices (zero-copy)
    let reader_data = DataReader::deserialize_from(&storage)?;
    
    println!("Numbers: {:?}", reader_data.numbers);  // [1, 2, 3, 4, 5]
    println!("Names: {:?}", reader_data.names);      // ["Alice", "Bob"]
    
    Ok(())
}
}

Performance Implications

TypeSerializationDeserializationMemory Usage
Vec<T>Moderate (iteration)Slow (allocation + copy)High (owned data)
&[T]Fast (direct copy)Fast (zero-copy)Low (borrowed data)

Best Practice: Use Vec<T> for data you own/modify, &[T] for read-only access.

2. Option vs Non-Option Interchangeability

Fields can be compatible between Option<T> and T under certain conditions:

#![allow(unused)]
fn main() {
// Serialization with Option
#[derive(FlatMessage)]
struct OptionalData {
    required_field: u32,
    optional_field: Option<String>,
    optional_number: Option<i64>,
}

// Deserialization without Option
#[derive(FlatMessage)]
struct RequiredData {
    required_field: u32,
    optional_field: String,  // Must exist in data
    optional_number: i64,    // Must exist in data
}

fn option_compatibility() -> Result<(), Error> {
    // Serialize with Some values
    let opt_data = OptionalData {
        required_field: 42,
        optional_field: Some("Hello".to_string()),
        optional_number: Some(123),
    };

    let mut storage = Storage::default();
    opt_data.serialize_to(&mut storage, Config::default())?;

    // Deserialize as required fields
    let req_data = RequiredData::deserialize_from(&storage)?;
    assert_eq!(req_data.optional_field, "Hello");
    assert_eq!(req_data.optional_number, 123);

    Ok(())
}
}

Compatibility Rules

Serialized AsDeserialized AsResult
Some(value)T✅ Works, gets value
NoneT❌ Error: field missing
TOption<T>✅ Works, gets Some(value)

Handling None Values

When deserializing None as a required field, you get an error:

#![allow(unused)]
fn main() {
fn handle_missing_optional() -> Result<(), Error> {
    let opt_data = OptionalData {
        required_field: 42,
        optional_field: None,       // This will cause an error
        optional_number: Some(123),
    };

    let mut storage = Storage::default();
    opt_data.serialize_to(&mut storage, Config::default())?;

    // This will fail because optional_field is None
    match RequiredData::deserialize_from(&storage) {
        Err(Error::FieldIsMissing(_)) => {
            println!("Field was None, can't deserialize as required");
        }
        _ => {}
    }

    Ok(())
}
}

3. String vs &str Interchangeability

Similar rules apply to strings:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct StringWriter {
    owned_text: String,
    optional_text: Option<String>,
}

#[derive(FlatMessage)]
struct StringReader<'a> {
    owned_text: &'a str,        // String -> &str (zero-copy)
    optional_text: &'a str,     // Option<String> -> &str (if Some)
}

fn string_example() -> Result<(), Error> {
    let writer = StringWriter {
        owned_text: "Hello World".to_string(),
        optional_text: Some("Optional text".to_string()),
    };

    let mut storage = Storage::default();
    writer.serialize_to(&mut storage, Config::default())?;

    let reader = StringReader::deserialize_from(&storage)?;
    println!("Text: {}", reader.owned_text);        // "Hello World"
    println!("Optional: {}", reader.optional_text); // "Optional text"

    Ok(())
}
}

Compatibility Matrix

SerializeDeserializeCompatiblePerformanceNotes
Vec<T>Vec<T>SlowCopy required
Vec<T>&[T]FastZero-copy
&[T]Vec<T>SlowCopy required
&[T]&[T]FastZero-copy
Some(T)TSame as TDirect access
NoneT-Error: field missing
TOption<T>Same as TWrapped in Some
String&strFastZero-copy
&strStringSlowCopy required

Default Values

There are several scenarios where a default value will be used to initialize a field during deserialization:

  1. The field is not mandatory and is not present in the serialized data.
  2. The field is present in the serialized data, but it has some issues trying to deserialize it and the validate attribute is set to fallback.
  3. The field is skipped during serialization and it has to be defaulted during deserialization.

In these scenarios, FlatMessage will do one of the following:

  1. Use the default value for the type if it is available (this implies that the type implements the Default trait).
  2. If the attribute default is specified, it will use the value of the attribute.

Example:

  1. Use the default value:
    #![allow(unused)]
    fn main() {
    #[derive(FlatMessage)]
    struct Test {
        #[flat_message_item(skip = true)]
        a: u32,
    }
    }
    In this case, the field a will be initialized to 0 (the default value for u32).
  2. Use the value of the attribute default:
    #![allow(unused)]
    fn main() {
    #[derive(FlatMessage)]
    struct Test {
        #[flat_message_item(skip = true, default = 10)]
        a: u32,
    }
    }
    In this case, the field a will be initialized to 10.

Custom Default Values

When using the attribute default, you can specify a custom default value for the field in the following ways:

  1. A constant value (e.g. default = 10 or default = MY_CONSTANT).
  2. A string representation of the value (e.g. default = "10" ). In this case the value is parsed and adjusted to fit the actual type of the field.
  3. A raw string representation of the value (e.g. default = r#"foo(1,2,3)"#). In this case the value is use exactly as is. This is in particular useul if you want to use exprssior or a call to a functon to initialize the value.

String representations

When using a string representation: default = "..." the following steps are checked:

  1. if the type is &str the value of the default attribute is kept as it is.
  2. if the type is String th value of the default attribute is converted into a String
  3. if the type is NOT a string the quotes (") are removed and the actual value will be used
  4. if the type is on option ( Option<T> ) then:
    • if the value of the default attribute is None then the field is set to None
    • if the value of the default attribute is Some(T) then the field is set to Some(T)
    • otherwise the value of the default attribute is converted into a Some<value>

Examples

TypeDefault valueActual value
Numeric (u8, u32, f32, etc)default = "10"10
Numeric (u8, u32, f32, etc)default = 123123
Numeric (u8, u32, f32, etc)default = MY_CONSTANTMY_CONSTANT (it is assumed that MY_CONSTANT exists in the current scope)
Numeric (u8, u32, f32, etc)default = r#"1+2+3"#6
Boolean value (bool)default = "true"true
Boolean value (bool)default = falsefalse
Boolean value (bool)default = r#"foo(a,b,c)"#foo(a,b,c) (it is assumed that a, b, c and the function foo exists in the current scope)
String reference ( &str )default = "hello""hello"
String ( String )default = "hello"String::from("hello")
String reference ( &str )default = MY_CONSTANT"MY_CONSTANT" (it is assumed that MY_CONSTANT exists in the current scope)
Optiondefault = "None"None
Optiondefault = "Some(123)"Some(123)
Optiondefault = MY_CONSTANTMY_CONSTANT (it is assumed that MY_CONSTANT exists in the current scope and it of type Option<T>)
Optiondefault = r#"foo(1+2+3)"#foo(1+2+3) (it is assumed that foo exists in the current scope and returns an Option<T>)
Optiondefault = "4"Some(4) (the value is automatically converted into a Some<T>)
Option<&str>default = "Hello"Some("Hello")
Optiondefault = "Hello"Some(String::from("Hello")) (first the content of the quotes is converted into a String, then it is converted into a Some<String>****)

Remark: If you need to be 100% sure that the value is converted into the correct type, you can use the raw string representation (e.g. default = r#"Some(1+2+3)"#).

Ignoring Fields

FlatMessage provides a powerful mechanism to exclude specific fields from serialization and deserialization using the ignore attribute. This feature is particularly useful for fields that contain runtime-only data, computed values, or zero-sized types that don't need to be persisted. Ignored fields have no impact on the binary representation. The serialized data will not contain any information about ignored fields.

Basic Usage

To ignore a field during serialization and deserialization, use the #[flat_message_item(ignore = true)] attribute:

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage)]
#[flat_message_options(store_name = false)]
struct User {
    id: u32,
    name: String,
    #[flat_message_item(ignore = true)]
    cached_score: u32,  // This field will be ignored
}
}

When serializing a User struct, the cached_score field will not be included in the binary data. During deserialization, this field will be set to its default value.

Alternative Syntax

FlatMessage also supports the skip attribute as an alias for ignore:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(store_name = false)]
struct Data {
    value: u8,
    #[flat_message_item(skip = true)]
    temp_data: u32,  // Equivalent to ignore = true
}
}

Both ignore = true and skip = true have identical behavior.

Default Values

Using Type's Default

When a field is ignored, it will be initialized with the type's default value during deserialization:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(store_name = false)]
struct Example {
    x: u8,
    #[flat_message_item(ignore = true)]
    y: u32,  // Will be 0 (u32's default) after deserialization
}

let data = Example { x: 1, y: 999 };
let mut storage = Storage::default();
data.serialize_to(&mut storage, Config::default()).unwrap();

let deserialized = Example::deserialize_from(&storage).unwrap();
assert_eq!(deserialized.x, 1);
assert_eq!(deserialized.y, 0);  // Default value, not 999
}

Custom Default Values

You can specify a custom default value for ignored fields using the default attribute:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(store_name = false)]
struct Config {
    version: u8,
    #[flat_message_item(ignore = true, default = 42)]
    cache_size: u32,  // Will be 42 after deserialization
}

let config = Config { version: 1, cache_size: 100 };
let mut storage = Storage::default();
config.serialize_to(&mut storage, Config::default()).unwrap();

let deserialized = Config::deserialize_from(&storage).unwrap();
assert_eq!(deserialized.version, 1);
assert_eq!(deserialized.cache_size, 42);  // Custom default value
}

Zero-Sized Types (ZST)

FlatMessage automatically ignores zero-sized types like PhantomData without requiring explicit attributes:

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
struct Container<T> {
    data: u8,
    _phantom: PhantomData<T>,  // Automatically ignored
}
}

Zero-sized types are treated as if they have ignore = true applied automatically.

Use Cases

Runtime-Only Data

Ignore fields that are only meaningful during runtime:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct Session {
    user_id: u32,
    created_at: u64,
    #[flat_message_item(ignore = true)]
    last_access_time: u64,  // Updated frequently, no need to persist
    #[flat_message_item(ignore = true)]
    is_active: bool,        // Runtime state
}
}

Computed Values

Ignore fields that can be computed from other data:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct Rectangle {
    width: f32,
    height: f32,
    #[flat_message_item(ignore = true, default = 0.0)]
    area: f32,  // Can be computed as width * height
}

impl Rectangle {
    fn new(width: f32, height: f32) -> Self {
        Self {
            width,
            height,
            area: width * height,
        }
    }
    
    fn compute_area(&mut self) {
        self.area = self.width * self.height;
    }
}
}

Temporary Buffers

Ignore fields used as temporary storage:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
struct DataProcessor {
    input: Vec<u8>,
    output: Vec<u8>,
    #[flat_message_item(ignore = true)]
    temp_buffer: Vec<u8>,  // Working space, no need to serialize
}
}

Performance Benefits

Ignoring unnecessary fields can improve performance by:

  • Reducing serialized data size
  • Decreasing serialization/deserialization time
  • Minimizing memory usage for persistent storage
  • Enabling better compression ratios

Checksum Validation

FlatMessage provides built-in data integrity features through checksums and various validation levels to ensure your data hasn't been corrupted during storage or transmission.

Checksum Basics

Enable checksums to detect data corruption:

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage, Debug, PartialEq)]
#[flat_message_options(checksum = true)]
struct ImportantData {
    value: u64,
    message: String,
}

fn checksum_example() -> Result<(), Error> {
    let data = ImportantData {
        value: 12345,
        message: "Critical information".to_string(),
    };

    let mut storage = Storage::default();
    data.serialize_to(&mut storage, Config::default())?;

    // Checksum is automatically calculated and stored
    println!("Serialized with checksum: {} bytes", storage.len());

    // Checksum is automatically verified during deserialization
    let restored = ImportantData::deserialize_from(&storage)?;
    assert_eq!(data, restored);

    Ok(())
}
}

Checksum Validation Modes

The validate_checksum option controls when checksums are validated. You can set it to:

  • always --> checksum should always be validated
  • never --> checksum is ignored
  • auto --> chcksum is checked only if present in the deserialized data
#![allow(unused)]
fn main() {
// Always validate checksums
#[derive(FlatMessage)]
#[flat_message_options(checksum = true, validate_checksum = "always")]
struct AlwaysValidated {
    data: Vec<u8>,
}

// Never validate checksums (performance optimization)
#[derive(FlatMessage)]
#[flat_message_options(checksum = true, validate_checksum = "never")]
struct NeverValidated {
    data: Vec<u8>,
}

// Auto validation (default) - validate only if checksum is present
#[derive(FlatMessage)]
#[flat_message_options(checksum = true, validate_checksum = "auto")]
struct AutoValidated {
    data: Vec<u8>,
}
}

Validation Mode Behavior

ModeChecksum PresentChecksum MissingBehavior
"always"✅ Validates❌ ErrorAlways requires checksum
"never"⚡ Skips✅ ContinuesNever validates
"auto"✅ Validates✅ ContinuesValidates if available

Corruption Detection

Checksums detect various types of data corruption:

#![allow(unused)]
fn main() {
fn corruption_detection_example() -> Result<(), Box<dyn std::error::Error>> {
    // Create and serialize data
    let original = ImportantData {
        value: 42,
        message: "Hello World".to_string(),
    };

    let mut storage = Storage::default();
    original.serialize_to(&mut storage, Config::default())?;

    // Simulate corruption by modifying bytes
    let mut corrupted_bytes = storage.as_slice().to_vec();
    corrupted_bytes[10] = 0xFF;  // Corrupt a byte
    let corrupted_storage = Storage::from_buffer(&corrupted_bytes);

    // Attempt to deserialize corrupted data
    match ImportantData::deserialize_from(&corrupted_storage) {
        Err(Error::InvalidChecksum((actual, expected))) => {
            println!("Corruption detected!");
            println!("Expected checksum: 0x{:08X}", expected);
            println!("Actual checksum: 0x{:08X}", actual);
        }
        Ok(_) => panic!("Should have detected corruption"),
        Err(e) => println!("Other error: {}", e),
    }

    Ok(())
}
}

Performance vs Safety Trade-offs

It is important to note that checksum validation are only performed when using deserialize_from(). If you use deserialize_from_unchecked(), the checksum validation is skipped (as the data is considered trusted).

This means that deserialize_from_unchecked() is always faster than deserialize_from(), as it skips the checksum validation and other checks, but it is unsafe as if used incorrectly, it can lead to data corruption.

Message Name Validation

FlatMessage provides a robust name validation system that allows you to control whether structure names are stored during serialization and validated during deserialization. This feature helps ensure type safety and prevents accidental deserialization of incompatible data structures.

Overview

The name validation system uses two key attributes that work together:

  • store_name: Controls whether the structure name hash is stored in the serialized data
  • validate_name: Controls whether the structure name is validated during deserialization

Attributes

store_name Attribute

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(store_name = true)]  // Default: true
struct MyStruct {
    value: u32,
}
}

When store_name = true:

  • A 32-bit hash of the structure name is stored in the serialized data
  • Adds 4 bytes to the serialized size
  • Enables name validation during deserialization

When store_name = false:

  • No name information is stored in the serialized data
  • Saves 4 bytes of storage space
  • Name validation is impossible during deserialization

validate_name Attribute

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(validate_name = true)] // Default: false
struct MyStruct {
    value: u32,
}
}

When validate_name = true:

  • During deserialization, the stored name hash is compared with the target structure's name
  • Throws Error::UnmatchedName if names don't match
  • Throws Error::NameNotStored if no name was stored during serialization

When validate_name = false (default):

  • No name validation is performed during deserialization
  • Allows deserialization between different structure types (if fields are compatible)

Attribute Combinations

1. Default Behavior: store_name = true, validate_name = false

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessage)]
struct StructA {
    value: u64,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
struct StructB {
    value: u64,
}
}

Behavior:

  • Name is stored during serialization (4 extra bytes)
  • No validation during deserialization
  • Cross-structure deserialization is allowed if fields are compatible
#![allow(unused)]
fn main() {
let a = StructA { value: 42 };
let mut storage = Storage::default();
a.serialize_to(&mut storage, Config::default()).unwrap();

// This works - same structure
let restored_a = StructA::deserialize_from(&storage).unwrap();

// This also works - different structure but compatible fields
let restored_b = StructB::deserialize_from(&storage).unwrap();
}

2. Strict Validation: store_name = true, validate_name = true

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = true, validate_name = true)]
struct StrictStruct {
    value: u64,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(validate_name = true)]
struct OtherStruct {
    value: u64,
}
}

Behavior:

  • Name is stored during serialization
  • Name is validated during deserialization
  • Cross-structure deserialization fails with Error::UnmatchedName
#![allow(unused)]
fn main() {
let strict = StrictStruct { value: 42 };
let mut storage = Storage::default();
strict.serialize_to(&mut storage, Config::default()).unwrap();

// This works - correct structure
let restored = StrictStruct::deserialize_from(&storage).unwrap();

// This fails - different structure name
match OtherStruct::deserialize_from(&storage) {
    Err(Error::UnmatchedName) => {
        println!("Name validation prevented cross-type deserialization");
    }
    _ => panic!("Expected name validation error"),
}
}

3. Space Optimized: store_name = false, validate_name = false

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false, validate_name = false)]
struct CompactStruct {
    value: u64,
}
}

Behavior:

  • No name is stored (saves 4 bytes)
  • No validation is performed
  • Maximum interoperability between compatible structures
#![allow(unused)]
fn main() {
let compact = CompactStruct { value: 42 };
let mut storage = Storage::default();
compact.serialize_to(&mut storage, Config::default()).unwrap();

// Works with any compatible structure
let restored = AnyCompatibleStruct::deserialize_from(&storage).unwrap();
}

4. Invalid Combination: store_name = false, validate_name = true

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false, validate_name = true)]
struct InvalidConfig {
    value: u64,
}

#[derive(Debug, PartialEq, Eq, FlatMessage)]
#[flat_message_options(store_name = false)]
struct DataSource {
    value: u64,
}
}

Behavior:

  • No name is stored during serialization
  • Validation is attempted during deserialization
  • Always fails with Error::NameNotStored
#![allow(unused)]
fn main() {
let source = DataSource { value: 42 };
let mut storage = Storage::default();
source.serialize_to(&mut storage, Config::default()).unwrap();

// This always fails because no name was stored
match InvalidConfig::deserialize_from(&storage) {
    Err(Error::NameNotStored) => {
        println!("Cannot validate name when none was stored");
    }
    _ => panic!("Expected NameNotStored error"),
}
}

Error Types

Error::UnmatchedName

Occurs when:

  • validate_name = true on the target structure
  • A name hash was stored in the serialized data
  • The stored name hash doesn't match the target structure's name hash

Error::NameNotStored

Occurs when:

  • validate_name = true on the target structure
  • No name hash was stored in the serialized data (source had store_name = false)

Use Cases

Type Safety in APIs

Use strict validation when deserializing data from external sources:

#![allow(unused)]
fn main() {
#[derive(FlatMessage)]
#[flat_message_options(store_name = true, validate_name = true)]
struct ApiRequest {
    user_id: u64,
    action: String,
}

fn handle_request(data: &[u8]) -> Result<(), Error> {
    // This will fail if the data wasn't serialized as ApiRequest
    let request = ApiRequest::deserialize_from(data)?;
    // Process request safely...
    Ok(())
}
}

Best Practices

  1. Use strict validation (store_name = true, validate_name = true) for:

    • Network protocol messages
    • API endpoints
    • Critical data structures where type safety is paramount
  2. Use flexible validation (store_name = true, validate_name = false) for:

    • Data persistence with potential schema evolution
    • Internal application data structures
    • When you need some metadata but want flexibility
  3. Disable name storage (store_name = false) for:

    • High-frequency data (sensor readings, telemetry)
    • Memory-constrained environments
    • When every byte counts and type safety is managed elsewhere
  4. Avoid the invalid combination (store_name = false, validate_name = true):

    • This configuration will always fail during deserialization
    • The compiler doesn't prevent this, so be careful with your configuration

Runtime Introspection

You can inspect stored names using StructureInformation:

#![allow(unused)]
fn main() {
use flat_message::*;

#[derive(FlatMessage)]
#[flat_message_options(store_name = true)]
struct MyStruct {
    value: u32,
}

fn inspect_name(storage: &Storage) -> Result<(), Error> {
    let info = StructureInformation::try_from(storage)?;
    
    if let Some(name) = info.name() {
        match name {
            name!("MyStruct") => println!("Found MyStruct data"),
            name!("OtherStruct") => println!("Found OtherStruct data"),
            _ => println!("Found unknown structure: {:?}", name),
        }
    } else {
        println!("No name information stored");
    }
    
    Ok(())
}
}

The name validation system provides a flexible balance between type safety, storage efficiency, and interoperability, allowing you to choose the right trade-offs for your specific use case.

Benchmarks

In terms of benchmarks, the following metrics can be used to evaluate a serialization/deserialization process:

  1. Speed - how fast is the serialization/deserialization process is compared to other serializers/deserializers
  2. Size - how much space is needed to store the serialized data (in particular for a schema-less serializer, like FlatMessage, the size needed to store the metadata is relevant)

Additionally, the for FlatMessage another relevant metrics are:

  1. Safe vs Unsafe deserialization (and their performance implications)
  2. Zero-copy deserialization (and its performance implications)
  3. FlatMessageStruct vs FlatMessagePacked for nested structures (and their performance implications in terms of speed and size)

Performance Results

The following tests were conducted against ofther serializers and deserializers crates:

  • performance - different structures were serialized and deserialized and the time needed for this operation was measured
  • size - the size of the serialized data was measured for different structures

Crates

The following crates were tested:

Crate / methodVersionSchema TypeObservation
flat_message0.1.0Schema-lessFor deserialization the deserialize(...) method is beng used
flat_message (⚠️)0.1.0Schema-less(Unchecked) For deserialization the deserialize_unchecked(...) method is beng used (meaning that no validation is done)
bincode2.0.1with Schemaalso use bincode_derive (2.0.1)
bson3.0.0Schema-less
flexbuffers25.2.10Schema-less
postcard1.1.3with Schema
serde_json1.0.143Schema-less
simd_json0.15.1Schema-less
ciborium0.2.2Schema-less
rmp0.8.14bothalso included rmp-serde for MessagePack (v1.3.0)
toml0.9.5Schema-lessTOML does not have a direct method to write into a buffer, so we write into a string and then copy that string into a buffer. This ads aditional cost for the algorithm.
protobuf (prost)0.14.1with SchemaProtobuf via prost crate. Not all tests are supported by protobuf (e.g. test that use u8, i8 or other unsuported types will be marked as N/A for protobuf).

Methodology

Each test consists doing the following for a chosen structure:

  • Ser Time - Serialize the structure for n times (repetitions) and measure the time needed to perform this operations
  • Deser Time - Deserialize a buffer containing the serialized data for n times (repetitions) and measure the time needed to perform this operations
  • Ser+Deser Time - Serialize and then deserialize the structure for n times (repetitions) and measure the time needed to perform this operations

The n parameter is usually a larger one (>1000) as usually de serialization/deserialization process is really fast and measuring it for a smaller number of times would not be representative.

Each repetition of "n" times is performed for "k" iterations and the times for each iterations are stored. From these, the median time is calculated. We prefer median time over average time as it is less sensitive to outliers.

The result for each tested structure (in terms of time) will be presended in the following way: median [min - mac]. For example: 1.5 [1.2 - 1.8] means that the median time is 1.5ms, the minimum time is 1.2ms and the maximum time is 1.8ms.

The following algorithm simulates how times are computed:

times = []
for iteration in 0..k {
    start = GetCurrentTime()
    for repetition in 0..n {
        Serialize(structure)
    }
    end = GetCurrentTime()
    times.push(end - start)
}
return (median(times), min(times), max(times))

For each structure we also compute the Data size (the minimum size required to store the data from that structure). That value is compared to the actual size of the serialized buffer. In most cases (since the serialized buffer is usually bigger than the data size) the percentage of increase is reported. The size value presented for each serialization method is presented as follows: size [+/- percentage]. For example: 355 [+69%] means that the size of the serialized buffer is 355 bytes and the data size is 209 bytes (so the percentage of increase is 69% for that method).

Remarks: It is important to highlight that some of the methods used are not schema-less (they will be marked with schema next to the name of the method). In these cases, it is possible that the actual size will be smaller than the data size (in particular if the serialization method compress some of the data)

OSes

The tests were performed on the following OSes:

  1. Windows - Windows 11, 64 bit,11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz (2.80 GHz), RAM 32.0 GB
  2. MacOS - MacOS 15.6.1 24G90 arm64, Apple M1 Pro, RAM 32.0 GB
  3. Linux - Kubuntu 24.04.3 LTS x86_64, kernel: 6.8.0-71-generic, 11th Gen Intel(R) Core(TM) i7-11850H (16) @ 4.80GHz , RAM 64.0 GB

Overall Speed

All of the above results are averaged over all the tested structures in the following way:

  • for each tested structure, we compute the speed (MB/sec) as the data size (bytes) * n (number of repetitions) / time (ms)
  • this is done for each OS and then the results are averaged over all the OSes

Remarks:

  • There are a lot of variation in the results - and while we did try to use a large variaty of structures, it is best to evaluate the results/structure as well and find the ones that are most appropiate to your use case.
  • Protobuf results are inconclusive as they were not aveaged on the entire set of structures.
AlgorithmWin (MB/sec)Mac (MB/sec)Linux (MB/sec)
FlatMessage (⚠️)4624.315143.916705.44
FlatMessage3888.784157.945072.87
protobuf (schema)2261.022357.242798.58
postcard (schema)2212.562726.472959.57
bincode (schema)2024.512478.932323.05
rmp (schema)1814.162110.852345.71
rmp1468.291721.221796.20
bson850.001089.001025.31
cbor756.17860.30853.52
flexbuffers410.41582.94494.43
simd_json377.15498.02464.32
json341.76479.47391.95
toml63.2070.7073.96

Multiple Fields

This benchmarks compares the performance of the different algorithms when serializing and deserializing a message with multiple fields. In particula for schema-less messages, this will show how well different message formats store the schema in the message. Fields have different basic types (string, u32, u64, i32, i64, f32, f64, bool, u8, i8, u16, i16).

#![allow(unused)]
fn main() {
pub struct MultipleFields {
    field_of_type_string: String,
    field_of_type_u32: u32,
    field_of_type_u64: u64,
    field_of_type_i32: i32,
    field_of_type_i64: i64,
    field_of_type_f32: f32,
    field_of_type_f64: f64,
    field_of_type_bool: bool,
    field_of_type_u8: u8,
    field_of_type_i8: i8,
    field_of_type_u16: u16,
    field_of_type_i16: i16,
    second_field_of_type_string: String,
    second_field_of_type_u32: u32,
    second_field_of_type_u64: u64,
    second_field_of_type_i32: i32,
    second_field_of_type_i64: i64,
    third_field_of_type_string: String,
    third_field_of_type_u32: u32,
    third_field_of_type_u64: u64,
    third_field_of_type_i32: i32,
    third_field_of_type_i64: i64,
    fourth_field_of_type_string: String,
    fourth_field_of_type_u32: u32,
    fourth_field_of_type_u64: u64,
    fourth_field_of_type_i32: i32,
    fourth_field_of_type_i64: i64,
}
}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 100000
  • Data size: 210 bytes
  • Protobuf: Not supported (due to the fields of type i8 and i16 that are not supported by protobuf)

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)355
[ +69%]
2.63
[ 2.20 - 2.92]
20.33
[ 17.33 - 22.89]
23.54
[ 20.05 - 26.37]
FlatMessage355
[ +69%]
2.77
[ 2.27 - 3.43]
23.87
[ 22.55 - 26.51]
27.36
[ 25.19 - 30.78]
bincode (schema)172
[ -19%]
10.03
[ 8.53 - 10.70]
29.41
[ 25.72 - 31.81]
39.87
[ 35.29 - 44.48]
postcard (schema)154
[ -27%]
10.47
[ 9.49 - 11.46]
30.39
[ 27.74 - 32.77]
42.47
[ 38.92 - 45.02]
rmp (schema)179
[ -15%]
13.60
[ 12.54 - 14.61]
38.97
[ 34.48 - 42.74]
59.93
[ 54.80 - 68.55]
rmp776
[+269%]
21.74
[ 18.31 - 22.99]
90.83
[ 78.58 - 99.07]
126.98
[112.18 - 138.83]
cbor786
[+274%]
47.90
[ 42.23 - 55.22]
169.09
[148.57 - 182.64]
222.42
[192.96 - 240.77]
json895
[+326%]
68.24
[ 58.92 - 74.31]
140.85
[122.75 - 151.39]
225.94
[198.50 - 246.99]
bson885
[+321%]
57.16
[ 51.87 - 62.13]
164.07
[144.80 - 178.24]
233.80
[204.84 - 259.61]
simd_json895
[+326%]
88.45
[ 76.05 - 99.23]
168.27
[151.13 - 181.42]
270.51
[248.37 - 295.02]
flexbuffers1022
[+386%]
439.06
[381.02 - 476.33]
181.15
[165.53 - 196.50]
646.95
[576.51 - 696.58]
toml894
[+325%]
369.89
[335.68 - 402.81]
856.84
[754.40 - 938.28]
1254.35
[1127.43 - 1386.98]
protobuf----

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)355
[ +69%]
3.04
[ 3.04 - 4.01]
10.44
[ 10.37 - 12.33]
13.46
[ 13.45 - 14.61]
FlatMessage355
[ +69%]
3.15
[ 3.05 - 7.92]
13.27
[ 13.18 - 17.01]
16.81
[ 16.75 - 20.53]
bincode (schema)172
[ -19%]
7.65
[ 7.64 - 7.88]
15.46
[ 15.35 - 15.85]
23.49
[ 23.40 - 24.18]
postcard (schema)154
[ -27%]
9.53
[ 9.52 - 9.60]
17.01
[ 16.93 - 17.19]
26.71
[ 26.66 - 26.80]
rmp (schema)179
[ -15%]
12.10
[ 12.10 - 12.32]
24.89
[ 24.84 - 24.98]
37.36
[ 37.26 - 37.59]
rmp776
[+269%]
21.10
[ 21.10 - 21.50]
58.73
[ 58.55 - 60.41]
80.55
[ 80.29 - 82.82]
json895
[+326%]
59.76
[ 59.59 - 61.49]
81.90
[ 81.72 - 84.49]
142.89
[142.56 - 147.54]
bson885
[+321%]
51.69
[ 51.63 - 52.08]
116.45
[116.16 - 117.56]
169.88
[169.48 - 172.46]
simd_json895
[+326%]
63.60
[ 63.40 - 65.63]
107.18
[106.12 - 110.47]
173.02
[171.21 - 177.92]
cbor786
[+274%]
42.49
[ 42.45 - 43.61]
132.44
[132.25 - 132.82]
176.32
[175.69 - 176.87]
flexbuffers1022
[+386%]
349.96
[348.16 - 359.48]
112.39
[111.88 - 119.31]
467.53
[466.12 - 480.27]
toml894
[+325%]
288.15
[283.89 - 295.04]
554.20
[552.77 - 557.26]
857.29
[855.34 - 864.52]
protobuf----

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)355
[ +69%]
2.73
[ 2.66 - 3.49]
7.52
[ 7.34 - 11.19]
10.22
[ 9.79 - 15.88]
FlatMessage355
[ +69%]
2.67
[ 2.34 - 3.86]
12.70
[ 11.92 - 19.66]
14.87
[ 14.51 - 21.55]
postcard (schema)154
[ -27%]
10.31
[ 9.99 - 12.47]
16.15
[ 15.66 - 19.02]
27.22
[ 26.14 - 32.00]
bincode (schema)172
[ -19%]
10.29
[ 9.74 - 10.96]
17.78
[ 16.95 - 35.14]
28.60
[ 27.73 - 39.15]
rmp (schema)179
[ -15%]
14.03
[ 13.40 - 23.77]
27.17
[ 26.72 - 36.25]
46.48
[ 44.50 - 50.72]
rmp776
[+269%]
21.56
[ 20.76 - 23.37]
69.45
[ 68.10 - 75.75]
95.85
[ 93.59 - 98.88]
json895
[+326%]
64.40
[ 62.01 - 69.40]
113.40
[109.06 - 127.41]
197.48
[184.91 - 247.76]
simd_json895
[+326%]
67.63
[ 62.81 - 74.22]
125.92
[117.82 - 146.19]
205.14
[193.67 - 269.61]
cbor786
[+274%]
46.78
[ 45.37 - 48.92]
151.96
[147.25 - 155.70]
207.72
[199.29 - 233.28]
bson885
[+321%]
54.75
[ 51.70 - 78.50]
144.14
[139.10 - 160.18]
208.93
[197.22 - 229.21]
flexbuffers1022
[+386%]
395.54
[376.59 - 449.20]
153.44
[150.59 - 165.30]
579.35
[544.29 - 630.75]
toml894
[+325%]
315.82
[297.91 - 379.37]
761.90
[735.18 - 812.90]
1101.13
[1050.93 - 1204.26]
protobuf----

Point

This benchmarks compares the performance of the different algorithms to serialize and deserialize a point structure. The idea is to see how well the algorithms handle the case of a small structure with 4 bytes alignment.

#![allow(unused)]
fn main() {
pub struct Point {
    x: i32,
    y: i32,
}
}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 500000
  • Data size: 8 bytes
  • Protobuf: Supported

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)26
[+225%]
0.99
[ 0.76 - 1.17]
3.30
[ 3.12 - 3.75]
3.76
[ 3.61 - 4.37]
FlatMessage26
[+225%]
0.91
[ 0.78 - 1.58]
3.57
[ 3.54 - 6.56]
4.49
[ 4.05 - 5.88]
postcard (schema)3
[ -63%]
3.78
[ 3.60 - 3.85]
2.93
[ 2.77 - 3.10]
6.95
[ 6.69 - 7.29]
bincode (schema)2
[ -75%]
2.76
[ 2.65 - 3.07]
3.15
[ 2.93 - 3.29]
7.07
[ 6.67 - 7.35]
rmp (schema)3
[ -63%]
5.74
[ 5.49 - 6.29]
5.15
[ 4.81 - 5.77]
11.09
[ 10.60 - 11.95]
rmp7
[ -13%]
6.93
[ 6.34 - 7.88]
10.51
[ 9.78 - 10.75]
16.60
[ 15.79 - 17.80]
protobuf (schema)13
[ +62%]
11.72
[ 10.77 - 12.29]
5.88
[ 5.39 - 6.27]
17.59
[ 16.44 - 18.29]
json17
[+112%]
11.30
[ 10.51 - 12.34]
24.70
[ 22.61 - 25.42]
39.57
[ 36.79 - 41.07]
bson19
[+137%]
17.92
[ 17.33 - 28.72]
43.09
[ 42.27 - 60.24]
60.15
[ 57.02 - 62.73]
cbor8
[ +0%]
14.51
[ 13.55 - 17.31]
56.70
[ 55.18 - 64.55]
68.12
[ 66.53 - 81.23]
simd_json17
[+112%]
10.86
[ 10.31 - 11.05]
191.43
[181.89 - 194.51]
207.16
[193.80 - 208.84]
flexbuffers17
[+112%]
189.42
[184.32 - 192.16]
44.84
[ 40.74 - 46.18]
251.47
[238.92 - 253.57]
toml16
[+100%]
157.98
[149.10 - 159.28]
286.86
[270.90 - 290.13]
476.62
[452.59 - 489.41]

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage26
[+225%]
1.05
[ 1.05 - 1.12]
2.46
[ 2.40 - 2.55]
2.84
[ 2.82 - 3.00]
FlatMessage (⚠️)26
[+225%]
1.01
[ 1.01 - 1.05]
2.21
[ 2.17 - 2.27]
2.84
[ 2.42 - 5.61]
postcard (schema)3
[ -63%]
3.10
[ 3.10 - 3.19]
1.16
[ 1.12 - 2.21]
3.72
[ 3.72 - 3.83]
bincode (schema)2
[ -75%]
2.06
[ 2.05 - 2.12]
2.64
[ 2.63 - 4.23]
4.41
[ 4.39 - 4.45]
rmp (schema)3
[ -63%]
4.63
[ 4.56 - 7.88]
4.62
[ 4.35 - 6.83]
9.05
[ 8.85 - 12.32]
rmp7
[ -13%]
4.96
[ 4.81 - 6.02]
10.46
[ 10.38 - 12.31]
15.25
[ 15.20 - 16.42]
protobuf (schema)13
[ +62%]
9.43
[ 9.13 - 10.31]
6.06
[ 6.04 - 6.13]
15.38
[ 15.24 - 15.48]
json17
[+112%]
11.64
[ 11.62 - 11.77]
20.35
[ 20.16 - 20.54]
31.77
[ 31.63 - 32.02]
bson19
[+137%]
12.40
[ 12.38 - 12.48]
24.88
[ 24.84 - 34.03]
41.56
[ 41.49 - 41.80]
cbor8
[ +0%]
13.18
[ 13.17 - 13.28]
62.83
[ 62.76 - 66.50]
77.80
[ 77.63 - 79.15]
flexbuffers17
[+112%]
99.77
[ 99.40 - 100.22]
29.26
[ 29.22 - 49.78]
139.39
[138.64 - 153.18]
simd_json17
[+112%]
8.64
[ 8.59 - 8.91]
129.03
[125.62 - 136.86]
139.43
[136.12 - 144.18]
toml16
[+100%]
81.83
[ 81.68 - 84.24]
220.94
[219.94 - 231.54]
325.22
[324.07 - 333.64]

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)26
[+225%]
0.82
[ 0.80 - 0.86]
3.50
[ 3.42 - 3.68]
3.91
[ 3.85 - 4.14]
FlatMessage26
[+225%]
0.84
[ 0.82 - 0.90]
3.95
[ 3.86 - 4.18]
4.25
[ 4.14 - 4.48]
postcard (schema)3
[ -63%]
3.84
[ 3.77 - 4.35]
3.11
[ 3.01 - 3.52]
6.93
[ 6.77 - 7.80]
bincode (schema)2
[ -75%]
2.89
[ 2.15 - 3.64]
3.18
[ 3.07 - 3.48]
7.76
[ 7.60 - 8.21]
rmp (schema)3
[ -63%]
6.01
[ 5.96 - 6.39]
5.21
[ 5.05 - 5.42]
11.41
[ 11.20 - 12.02]
rmp7
[ -13%]
7.30
[ 6.99 - 7.74]
11.07
[ 10.78 - 11.70]
17.45
[ 17.09 - 18.37]
protobuf (schema)13
[ +62%]
12.05
[ 11.74 - 12.80]
5.91
[ 5.74 - 6.42]
18.02
[ 17.50 - 19.19]
json17
[+112%]
10.68
[ 10.50 - 12.46]
22.46
[ 22.05 - 23.62]
34.82
[ 34.23 - 60.41]
bson19
[+137%]
16.97
[ 16.66 - 17.28]
42.52
[ 41.85 - 47.99]
60.10
[ 58.70 - 69.23]
cbor8
[ +0%]
15.81
[ 15.70 - 16.18]
59.24
[ 58.39 - 61.95]
69.63
[ 68.21 - 70.32]
simd_json17
[+112%]
10.24
[ 9.92 - 10.54]
112.17
[102.62 - 155.85]
127.21
[118.65 - 164.30]
flexbuffers17
[+112%]
110.84
[108.56 - 114.05]
45.89
[ 45.12 - 71.19]
173.46
[168.91 - 198.21]
toml16
[+100%]
95.85
[ 93.15 - 102.19]
256.90
[247.26 - 288.81]
379.74
[371.73 - 462.67]

Long Strings

This benchmarks compares the performance of the different algorithms when dealing with a structure that contains multiple long strings. Each of the string will be instantiated with large strings (between 100 and 2000 characters) making the total size of the structure close to 4000 bytes.

#![allow(unused)]
fn main() {
pub struct LongStringStructure {
    string_one: String,
    string_two: String,
    string_three: String,
    string_four: String,
    value_one: u32,
    value_two: u64,
}
}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 100000
  • Data size: 3919 bytes
  • Protobuf: Supported

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)3968
[ +1%]
7.32
[ 6.31 - 8.86]
20.41
[ 19.69 - 27.33]
28.22
[ 26.88 - 35.43]
postcard (schema)3915
[ -1%]
6.56
[ 5.96 - 9.44]
33.91
[ 31.59 - 41.96]
39.55
[ 37.61 - 44.44]
FlatMessage3968
[ +1%]
6.65
[ 5.92 - 9.10]
34.61
[ 32.12 - 42.88]
40.63
[ 35.66 - 49.72]
bincode (schema)3920
[ +0%]
6.23
[ 5.56 - 8.26]
36.49
[ 34.02 - 43.98]
43.08
[ 40.01 - 52.90]
rmp (schema)3922
[ +0%]
5.89
[ 5.33 - 8.05]
36.31
[ 34.14 - 46.12]
43.21
[ 40.46 - 56.13]
protobuf (schema)3921
[ +0%]
7.01
[ 6.50 - 8.47]
36.34
[ 33.69 - 43.30]
43.76
[ 40.96 - 52.60]
rmp3989
[ +1%]
6.71
[ 6.22 - 9.30]
45.51
[ 41.83 - 59.64]
54.31
[ 51.52 - 69.65]
bson4014
[ +2%]
13.09
[ 12.06 - 17.22]
54.05
[ 50.04 - 66.43]
71.57
[ 68.23 - 85.13]
cbor3989
[ +1%]
11.26
[ 10.88 - 15.10]
78.63
[ 77.29 - 99.97]
91.99
[ 88.11 - 120.60]
flexbuffers4041
[ +3%]
107.08
[101.70 - 128.89]
66.68
[ 64.52 - 77.82]
185.35
[180.24 - 219.79]
simd_json4011
[ +2%]
27.61
[ 25.73 - 28.45]
207.88
[195.33 - 236.90]
246.34
[231.25 - 276.01]
json4011
[ +2%]
179.69
[170.16 - 207.78]
113.51
[104.95 - 122.49]
298.94
[283.15 - 316.62]
toml4010
[ +2%]
821.39
[786.00 - 907.08]
758.25
[713.60 - 796.55]
1638.54
[1545.04 - 1743.35]

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)3968
[ +1%]
10.12
[ 10.12 - 10.19]
18.68
[ 17.35 - 19.51]
28.32
[ 27.03 - 28.98]
postcard (schema)3915
[ -1%]
7.23
[ 7.11 - 7.84]
30.47
[ 30.02 - 31.90]
37.92
[ 36.72 - 38.88]
rmp (schema)3922
[ +0%]
6.89
[ 6.88 - 6.91]
33.27
[ 32.86 - 34.80]
40.72
[ 40.06 - 42.12]
FlatMessage3968
[ +1%]
10.27
[ 10.17 - 16.82]
32.23
[ 31.65 - 42.18]
41.50
[ 41.16 - 43.73]
bincode (schema)3920
[ +0%]
7.25
[ 7.24 - 7.33]
36.81
[ 34.94 - 37.38]
44.04
[ 41.72 - 44.70]
protobuf (schema)3921
[ +0%]
8.71
[ 8.67 - 8.79]
35.99
[ 34.19 - 37.48]
44.91
[ 43.05 - 46.63]
rmp3989
[ +1%]
8.27
[ 8.26 - 8.39]
40.59
[ 39.59 - 42.12]
49.34
[ 48.31 - 50.44]
bson4014
[ +2%]
13.56
[ 13.52 - 13.59]
44.21
[ 43.06 - 44.91]
58.75
[ 57.73 - 59.61]
cbor3989
[ +1%]
12.84
[ 12.82 - 12.89]
72.15
[ 71.56 - 73.79]
86.91
[ 86.36 - 88.37]
flexbuffers4041
[ +3%]
71.80
[ 67.99 - 72.59]
53.19
[ 52.43 - 54.70]
133.62
[132.45 - 135.85]
simd_json4011
[ +2%]
42.54
[ 42.40 - 43.27]
147.50
[145.23 - 149.58]
191.22
[188.72 - 194.15]
json4011
[ +2%]
139.28
[138.95 - 139.90]
85.05
[ 84.74 - 87.02]
225.44
[224.47 - 229.65]
toml4010
[ +2%]
1064.13
[1059.03 - 1069.45]
706.01
[702.41 - 722.81]
1799.29
[1773.97 - 1810.07]

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)3968
[ +1%]
6.69
[ 6.32 - 8.26]
12.75
[ 10.84 - 17.90]
19.76
[ 17.99 - 21.71]
postcard (schema)3915
[ -1%]
6.42
[ 6.20 - 8.35]
25.42
[ 23.34 - 29.29]
31.06
[ 28.77 - 34.33]
FlatMessage3968
[ +1%]
7.16
[ 6.73 - 8.93]
25.49
[ 23.42 - 36.57]
32.13
[ 30.21 - 48.09]
rmp (schema)3922
[ +0%]
5.79
[ 5.45 - 6.37]
26.91
[ 25.74 - 43.60]
32.41
[ 31.64 - 35.81]
protobuf (schema)3921
[ +0%]
6.72
[ 6.48 - 7.09]
29.34
[ 25.49 - 31.26]
36.55
[ 33.42 - 48.14]
bincode (schema)3920
[ +0%]
6.20
[ 5.90 - 6.60]
34.71
[ 31.58 - 37.22]
40.62
[ 37.18 - 43.31]
rmp3989
[ +1%]
6.52
[ 6.26 - 6.94]
36.78
[ 35.60 - 39.13]
43.45
[ 42.51 - 47.37]
bson4014
[ +2%]
13.06
[ 12.35 - 18.81]
42.50
[ 38.38 - 44.33]
58.51
[ 53.93 - 61.10]
cbor3989
[ +1%]
11.56
[ 10.97 - 15.93]
67.95
[ 63.18 - 75.39]
81.07
[ 75.50 - 118.26]
flexbuffers4041
[ +3%]
84.05
[ 73.17 - 99.47]
56.81
[ 51.97 - 78.07]
149.65
[137.27 - 157.13]
simd_json4011
[ +2%]
27.65
[ 26.27 - 30.50]
172.07
[163.45 - 203.11]
204.12
[196.88 - 219.06]
json4011
[ +2%]
184.73
[178.11 - 234.89]
94.42
[ 89.33 - 104.79]
280.38
[271.74 - 289.52]
toml4010
[ +2%]
767.95
[743.80 - 807.93]
672.11
[644.87 - 702.78]
1475.63
[1411.00 - 1501.73]

Large Vectors

This benchmarks compares the performance of the different algorithms when dealing with a structure that contains multiple large vectors. The vectors will be instantiated as follows:

  • ints: 2000 elements between 200 and 220
  • floats: 10000 elements between -1000000.0 and 1000000.0
  • uints: 25000 elements between 0 and 1000000
  • doubles: 30000 elements between 0.0 and 1000000.0
#![allow(unused)]
fn main() {
pub struct LargeVectors {
    ints: Vec<i32>,
    floats: Vec<f32>,
    uints: Vec<u32>,
    doubles: Vec<f64>,
}
}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 100
  • Data size: 388008 bytes
  • Protobuf: Supported

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage388060
[ +0%]
1.40
[ 1.31 - 2.61]
1.32
[ 1.12 - 13.24]
2.65
[ 2.28 - 13.78]
FlatMessage (⚠️)388060
[ +0%]
1.35
[ 1.15 - 2.55]
1.37
[ 1.00 - 14.00]
2.68
[ 2.37 - 12.95]
bincode (schema)407012
[ +4%]
15.50
[ 13.72 - 25.95]
14.02
[ 11.95 - 23.81]
29.96
[ 25.90 - 46.78]
postcard (schema)358260
[ -8%]
20.11
[ 17.25 - 24.88]
10.63
[ 9.43 - 15.52]
30.74
[ 26.41 - 42.54]
protobuf (schema)358265
[ -8%]
17.05
[ 14.16 - 39.22]
15.39
[ 13.50 - 29.45]
32.28
[ 27.52 - 64.30]
rmp445039
[ +14%]
16.85
[ 14.12 - 33.93]
27.71
[ 23.07 - 48.36]
45.39
[ 38.31 - 75.90]
rmp (schema)445013
[ +14%]
16.50
[ 13.82 - 32.05]
27.73
[ 25.33 - 52.29]
45.46
[ 39.83 - 91.06]
cbor320339
[ -18%]
49.73
[ 43.10 - 64.13]
94.20
[ 84.26 - 111.31]
145.84
[131.47 - 170.92]
flexbuffers264099
[ -32%]
66.30
[ 58.72 - 98.01]
110.55
[ 97.93 - 156.83]
180.88
[157.36 - 243.98]
json539243
[ +38%]
265.55
[227.27 - 308.44]
99.88
[ 94.64 - 116.47]
353.35
[330.89 - 417.55]
bson960615
[+147%]
190.55
[157.87 - 219.51]
244.22
[204.61 - 285.33]
432.94
[371.42 - 504.68]
simd_json539243
[ +38%]
286.91
[262.48 - 337.45]
177.50
[167.55 - 199.11]
482.78
[433.14 - 559.85]
toml606238
[ +56%]
350.72
[314.07 - 397.58]
1259.97
[1051.49 - 1489.37]
1622.38
[1398.75 - 2618.01]

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)388060
[ +0%]
0.93
[ 0.89 - 1.03]
0.71
[ 0.67 - 0.92]
2.70
[ 2.45 - 2.81]
FlatMessage388060
[ +0%]
0.96
[ 0.93 - 1.13]
0.73
[ 0.68 - 0.83]
2.87
[ 2.69 - 2.94]
postcard (schema)358260
[ -8%]
13.56
[ 13.41 - 14.09]
6.30
[ 6.19 - 8.04]
19.89
[ 19.85 - 22.84]
bincode (schema)407012
[ +4%]
9.23
[ 9.21 - 9.53]
10.84
[ 8.63 - 14.85]
21.19
[ 17.81 - 23.30]
protobuf (schema)358265
[ -8%]
10.77
[ 10.75 - 11.20]
19.54
[ 17.00 - 20.37]
30.52
[ 27.59 - 31.33]
rmp (schema)445013
[ +14%]
17.62
[ 17.35 - 17.97]
31.95
[ 31.46 - 33.94]
50.05
[ 49.24 - 54.93]
rmp445039
[ +14%]
17.35
[ 17.31 - 20.36]
33.35
[ 31.54 - 37.02]
51.29
[ 49.43 - 54.87]
flexbuffers264099
[ -32%]
60.09
[ 58.93 - 61.57]
61.73
[ 59.33 - 67.41]
125.72
[123.06 - 134.17]
cbor320339
[ -18%]
46.99
[ 46.91 - 48.50]
81.21
[ 80.76 - 83.73]
128.18
[127.71 - 153.43]
bson960615
[+147%]
118.51
[118.05 - 122.29]
153.30
[149.42 - 157.42]
274.78
[266.38 - 295.11]
json539243
[ +38%]
194.88
[193.23 - 231.19]
82.23
[ 78.61 - 85.68]
280.23
[272.33 - 286.06]
simd_json539243
[ +38%]
214.50
[212.87 - 247.29]
98.83
[ 95.00 - 120.02]
317.99
[313.92 - 326.43]
toml606238
[ +56%]
202.20
[200.94 - 213.92]
672.79
[658.82 - 697.97]
873.71
[862.31 - 902.96]

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)388060
[ +0%]
1.22
[ 1.16 - 1.54]
0.76
[ 0.72 - 1.03]
2.13
[ 2.04 - 2.74]
FlatMessage388060
[ +0%]
1.26
[ 1.23 - 1.41]
0.87
[ 0.76 - 6.82]
2.25
[ 2.20 - 8.84]
bincode (schema)407012
[ +4%]
14.85
[ 14.56 - 17.53]
11.55
[ 11.24 - 12.21]
26.87
[ 25.97 - 28.12]
postcard (schema)358260
[ -8%]
18.27
[ 18.04 - 18.98]
9.13
[ 8.96 - 9.41]
27.82
[ 27.22 - 28.42]
protobuf (schema)358265
[ -8%]
15.06
[ 14.80 - 15.38]
13.69
[ 13.37 - 13.83]
28.67
[ 27.97 - 30.95]
rmp445039
[ +14%]
14.73
[ 14.50 - 20.30]
31.12
[ 30.26 - 50.68]
46.66
[ 44.98 - 74.95]
rmp (schema)445013
[ +14%]
13.87
[ 13.68 - 16.92]
31.77
[ 30.92 - 34.81]
47.12
[ 45.69 - 58.30]
cbor320339
[ -18%]
53.78
[ 53.01 - 75.71]
89.73
[ 87.73 - 121.95]
145.53
[142.69 - 153.50]
flexbuffers264099
[ -32%]
58.48
[ 57.33 - 73.69]
107.12
[106.37 - 142.04]
165.70
[162.62 - 221.79]
json539243
[ +38%]
194.15
[191.85 - 200.21]
95.20
[ 94.02 - 97.09]
291.55
[284.73 - 343.34]
simd_json539243
[ +38%]
221.01
[218.67 - 237.59]
110.15
[106.49 - 192.84]
333.07
[329.37 - 396.38]
bson960615
[+147%]
178.64
[172.46 - 186.55]
218.06
[212.31 - 234.52]
403.27
[388.82 - 439.16]
toml606238
[ +56%]
307.17
[301.41 - 389.19]
898.43
[881.22 - 975.59]
1215.07
[1176.29 - 1272.70]

Large Vectors

This benchmarks checks to see how well a structure containing different enum fields can be serialized and deserialized. The enum have different sized variants (u8, u32, i64) and are instantiated with different values.

#![allow(unused)]
fn main() {
#[repr(u8)]
enum Color {
    Red = 1,
    Green = 2,
    Blue = 3,
    Yellow = 100,
    Cyan = 101,
    Magenta = 102,
}
#[repr(u32)]
enum Math {
    A = 1,
    B = 1000,
    C = 1000000,
    D = 1000000000,
}
#[repr(i64)]
enum Negative {
    A = 1,
    B = -1000,
    C = 1000000,
    D = -1000000000,
    E = 1000000000000,
    F = -1000000000000000,
}
pub struct EnumFields {
    col: Color,
    math: Math,
    neg: Negative,
}
}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 500000
  • Data size: 13 bytes
  • Protobuf: Not Supported (enums can't be used directly in protobuf via prost crate)

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
postcard (schema)3
[ -77%]
2.45
[ 1.70 - 2.97]
4.66
[ 2.93 - 4.84]
6.38
[ 4.24 - 6.54]
FlatMessage (⚠️)51
[+292%]
2.41
[ 1.71 - 2.52]
5.84
[ 4.17 - 7.11]
7.04
[ 5.11 - 8.29]
FlatMessage51
[+292%]
2.18
[ 1.44 - 2.65]
6.18
[ 4.15 - 7.37]
7.10
[ 5.09 - 8.72]
bincode (schema)3
[ -77%]
6.80
[ 4.79 - 8.42]
8.00
[ 5.91 - 8.98]
17.42
[ 12.30 - 19.38]
rmp (schema)13
[ +0%]
5.62
[ 4.19 - 6.69]
27.57
[ 20.13 - 32.89]
34.42
[ 25.33 - 40.73]
rmp26
[+100%]
7.44
[ 5.63 - 8.74]
46.45
[ 34.02 - 54.01]
53.70
[ 39.64 - 62.60]
json38
[+192%]
33.50
[ 24.57 - 37.36]
78.92
[ 58.62 - 91.34]
112.82
[ 82.15 - 127.90]
bson45
[+246%]
42.67
[ 30.45 - 48.13]
102.22
[ 72.41 - 113.37]
147.86
[107.70 - 165.23]
cbor26
[+100%]
29.30
[ 21.86 - 33.01]
158.47
[116.82 - 182.08]
185.83
[129.80 - 209.97]
simd_json38
[+192%]
36.12
[ 24.98 - 49.41]
330.43
[220.58 - 493.58]
384.26
[243.10 - 545.29]
flexbuffers44
[+238%]
383.99
[279.44 - 433.44]
101.35
[ 75.51 - 112.54]
545.00
[413.95 - 628.83]
toml37
[+184%]
469.24
[307.22 - 492.86]
580.37
[386.81 - 633.13]
1073.73
[754.72 - 1187.96]
protobuf----

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
postcard (schema)3
[ -77%]
1.68
[ 1.63 - 2.85]
2.08
[ 1.88 - 2.27]
4.52
[ 3.64 - 4.85]
FlatMessage51
[+292%]
1.83
[ 1.81 - 1.85]
3.93
[ 3.90 - 4.04]
5.09
[ 5.00 - 5.88]
FlatMessage (⚠️)51
[+292%]
1.78
[ 1.76 - 1.83]
2.76
[ 2.66 - 4.52]
5.92
[ 5.13 - 7.49]
bincode (schema)3
[ -77%]
3.55
[ 3.52 - 3.62]
4.12
[ 3.96 - 4.22]
7.46
[ 7.27 - 7.59]
rmp (schema)13
[ +0%]
3.76
[ 3.65 - 3.85]
16.15
[ 15.87 - 16.71]
20.63
[ 19.79 - 20.94]
rmp26
[+100%]
4.97
[ 4.76 - 5.11]
30.33
[ 29.55 - 30.88]
34.20
[ 33.89 - 35.07]
json38
[+192%]
24.06
[ 23.90 - 24.74]
36.92
[ 36.70 - 37.95]
60.92
[ 60.13 - 62.20]
bson45
[+246%]
27.89
[ 27.38 - 28.39]
49.57
[ 49.15 - 51.08]
81.31
[ 80.06 - 82.29]
cbor26
[+100%]
17.40
[ 17.26 - 17.79]
102.34
[101.03 - 104.12]
121.39
[119.61 - 123.23]
simd_json38
[+192%]
26.13
[ 25.75 - 26.77]
193.82
[186.38 - 214.73]
220.08
[213.59 - 253.01]
flexbuffers44
[+238%]
161.10
[158.31 - 163.03]
67.87
[ 66.92 - 69.34]
234.97
[232.07 - 261.17]
toml37
[+184%]
195.04
[192.15 - 197.56]
295.58
[288.06 - 300.80]
515.11
[499.74 - 539.25]
protobuf----

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
postcard (schema)3
[ -77%]
1.90
[ 1.86 - 2.01]
3.33
[ 3.28 - 3.43]
4.78
[ 4.72 - 4.87]
FlatMessage (⚠️)51
[+292%]
2.06
[ 1.67 - 2.31]
4.35
[ 4.24 - 5.20]
5.14
[ 4.53 - 6.42]
FlatMessage51
[+292%]
2.08
[ 1.58 - 2.55]
4.29
[ 4.19 - 6.00]
5.23
[ 4.88 - 7.01]
bincode (schema)3
[ -77%]
2.34
[ 2.30 - 2.75]
6.08
[ 5.93 - 7.04]
10.57
[ 10.39 - 12.25]
rmp (schema)13
[ +0%]
3.99
[ 3.79 - 5.62]
22.86
[ 22.52 - 27.89]
28.12
[ 27.63 - 30.23]
rmp26
[+100%]
5.35
[ 5.28 - 5.50]
38.55
[ 37.46 - 40.42]
45.84
[ 44.45 - 52.82]
json38
[+192%]
24.12
[ 23.71 - 24.78]
44.71
[ 43.98 - 46.08]
70.54
[ 68.76 - 104.62]
bson45
[+246%]
28.20
[ 27.70 - 29.17]
77.89
[ 76.43 - 79.92]
108.27
[107.16 - 112.51]
cbor26
[+100%]
22.64
[ 22.09 - 23.73]
118.84
[116.84 - 138.67]
137.75
[135.90 - 145.47]
simd_json38
[+192%]
21.20
[ 20.89 - 28.28]
128.36
[124.29 - 133.47]
158.68
[153.74 - 184.81]
flexbuffers44
[+238%]
176.53
[169.48 - 187.20]
79.01
[ 77.30 - 110.28]
288.10
[279.43 - 292.04]
toml37
[+184%]
217.34
[213.23 - 248.23]
343.34
[335.26 - 387.61]
649.56
[637.36 - 683.45]
protobuf----

Large Vectors

This benchmarks checks to see how well a structure containing fields of type Option<T> can be serialized and deserialized. The structure is initialized as follows:

  • opt_one is initialized to Some("Hello, World - this is an option field")
  • opt_two is initialized to Some(12345678)
  • opt_three is initialized to None
  • opt_four is initialized to Some([1, 2, 3, 4, 100, 200, 300, 400, 1000, 2000, 3000, 4000, 10000, 20000, 30000, 40000])
  • opt_five is initialized to Some(["Hello", "World", "This", "is", "an", "option", "field"])
  • opt_six is initialized to None
  • opt_seven is initialized to None
#![allow(unused)]
fn main() {
pub struct OptionFields {
    opt_one: Option<String>,
    opt_two: Option<u32>,
    opt_three: Option<bool>,
    opt_four: Option<Vec<u32>>,
    opt_five: Option<Vec<String>>,
    opt_six: Option<String>,
    opt_seven: Option<Vec<u32>>,
}
}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 100000
  • Data size: 145 bytes
  • Protobuf: Not Supported (Option is not supported in protobuf via prost crate)

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)191
[ +31%]
4.26
[ 3.78 - 4.86]
37.45
[ 34.92 - 44.13]
43.90
[ 40.06 - 50.40]
FlatMessage191
[ +31%]
4.52
[ 3.81 - 5.04]
45.98
[ 44.20 - 53.71]
50.69
[ 47.01 - 56.24]
postcard (schema)118
[ -19%]
12.51
[ 11.52 - 14.10]
48.39
[ 44.57 - 53.89]
64.46
[ 58.04 - 69.73]
bincode (schema)125
[ -14%]
11.43
[ 10.40 - 13.85]
54.60
[ 50.89 - 63.12]
70.15
[ 63.19 - 79.04]
rmp (schema)126
[ -14%]
9.33
[ 8.37 - 10.89]
62.90
[ 57.03 - 72.54]
74.37
[ 68.82 - 86.99]
rmp188
[ +29%]
10.82
[ 9.89 - 12.04]
72.12
[ 68.27 - 82.56]
86.56
[ 82.02 - 98.82]
json264
[ +82%]
23.04
[ 21.89 - 26.13]
118.43
[110.81 - 135.33]
149.71
[142.48 - 169.75]
simd_json264
[ +82%]
26.31
[ 25.21 - 29.14]
153.30
[149.29 - 205.95]
188.02
[173.91 - 241.76]
cbor187
[ +28%]
26.08
[ 24.49 - 30.69]
161.60
[150.53 - 198.38]
190.97
[183.57 - 335.13]
bson402
[+177%]
67.99
[ 63.65 - 86.69]
152.11
[143.13 - 185.71]
232.28
[217.78 - 269.26]
flexbuffers254
[ +75%]
151.98
[145.21 - 176.35]
116.80
[109.76 - 128.22]
282.20
[264.99 - 322.41]
toml235
[ +62%]
157.57
[143.44 - 171.29]
431.05
[401.97 - 472.36]
601.73
[564.37 - 655.63]
protobuf----

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)191
[ +31%]
4.63
[ 4.53 - 4.68]
20.67
[ 20.26 - 23.96]
25.26
[ 25.00 - 25.82]
FlatMessage191
[ +31%]
4.79
[ 4.54 - 18.56]
25.49
[ 24.83 - 28.69]
30.17
[ 29.54 - 31.25]
bincode (schema)125
[ -14%]
10.37
[ 10.19 - 30.40]
29.61
[ 29.11 - 30.90]
40.65
[ 40.08 - 41.13]
postcard (schema)118
[ -19%]
12.40
[ 12.31 - 12.49]
28.07
[ 27.69 - 28.48]
40.99
[ 40.18 - 41.32]
rmp (schema)126
[ -14%]
10.60
[ 10.39 - 10.74]
37.17
[ 36.80 - 37.78]
47.63
[ 47.19 - 48.06]
rmp188
[ +29%]
11.88
[ 11.72 - 12.11]
44.46
[ 44.05 - 45.68]
56.63
[ 56.12 - 56.98]
json264
[ +82%]
23.77
[ 23.49 - 23.87]
68.47
[ 67.91 - 68.60]
92.42
[ 92.15 - 92.85]
simd_json264
[ +82%]
27.25
[ 26.82 - 27.53]
104.36
[103.07 - 107.31]
131.90
[131.54 - 192.58]
cbor187
[ +28%]
24.08
[ 23.75 - 24.39]
121.37
[120.49 - 122.90]
146.94
[145.62 - 147.81]
bson402
[+177%]
52.94
[ 52.37 - 53.36]
101.40
[100.55 - 120.19]
159.05
[158.11 - 160.64]
flexbuffers254
[ +75%]
107.38
[106.93 - 108.01]
73.84
[ 73.58 - 74.24]
186.75
[186.05 - 187.50]
toml235
[ +62%]
117.48
[116.20 - 118.65]
282.25
[280.15 - 286.42]
408.69
[406.48 - 432.76]
protobuf----

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)191
[ +31%]
3.42
[ 3.10 - 3.69]
12.64
[ 12.20 - 14.05]
16.34
[ 15.39 - 18.91]
FlatMessage191
[ +31%]
3.14
[ 2.98 - 3.51]
18.85
[ 17.88 - 21.14]
23.02
[ 22.31 - 24.93]
postcard (schema)118
[ -19%]
11.79
[ 11.12 - 12.19]
22.31
[ 21.80 - 23.84]
34.81
[ 34.09 - 37.21]
bincode (schema)125
[ -14%]
10.25
[ 10.10 - 11.30]
28.59
[ 27.92 - 31.71]
42.14
[ 41.50 - 46.19]
rmp (schema)126
[ -14%]
8.23
[ 8.04 - 9.26]
34.27
[ 33.12 - 37.70]
44.33
[ 43.24 - 49.31]
rmp188
[ +29%]
9.28
[ 9.05 - 10.38]
42.29
[ 41.09 - 47.13]
55.92
[ 54.69 - 62.79]
json264
[ +82%]
22.23
[ 21.78 - 26.22]
73.41
[ 71.95 - 81.41]
105.77
[ 97.07 - 133.97]
simd_json264
[ +82%]
26.10
[ 24.60 - 37.97]
94.13
[ 88.75 - 99.57]
124.87
[119.99 - 134.12]
cbor187
[ +28%]
25.62
[ 24.91 - 27.00]
127.50
[124.79 - 133.87]
154.28
[152.52 - 165.43]
bson402
[+177%]
63.22
[ 62.09 - 66.58]
113.48
[111.71 - 120.82]
186.09
[183.00 - 244.09]
flexbuffers254
[ +75%]
118.33
[113.95 - 136.28]
83.15
[ 82.46 - 92.44]
230.28
[209.08 - 262.75]
toml235
[ +62%]
131.17
[126.57 - 164.83]
353.57
[344.60 - 394.50]
527.41
[500.84 - 586.29]
protobuf----

Large Vectors

This benchmarks checks to see how well a structure containing variant fields can be serialized and deserialized. The fields are instantiated with different values, as follows:

  • v1 is initialized to MyVariant::U32(0x12345)
  • v2 is initialized to MyVariant::U64(0x1234567890)
  • v3 is initialized to MyVariant::String(String::from("Hello, World!"))
  • v4 is initialized to MyVariant::Vector(vec![1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500, 1000, 2000, 3000, 4000, 5000])
  • v5 is initialized to MyVariant::StringVector(vec![String::from("Hello"), String::from("World"), String::from("This"), String::from("is"), String::from("a"), String::from("test")])
  • v6 is initialized to MyVariant::SimpleVariant
  • v7 is initialized to MyVariant::U32(0)
  • v8 is initialized to MyVariant::U64(100)
  • v9 is initialized to None
  • v10 is initialized to Some(MyVariant::String(String::from("Hello, World! Testing a variant in a option field").repeat(100)))
#![allow(unused)]
fn main() {
enum MyVariant {
    U32(u32),
    U64(u64),
    String(String),
    Vector(Vec<u32>),
    StringVector(Vec<String>),
    SimpleVariant,
}

pub struct VariantFields {
    v1: MyVariant,
    v2: MyVariant,
    v3: MyVariant,
    v4: MyVariant,
    v5: MyVariant,
    v6: MyVariant,
    v7: MyVariant,
    v8: MyVariant,
    v9: Option<MyVariant>,
    v10: Option<MyVariant>,
}
}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 50000
  • Data size: 5048 bytes
  • Protobuf: Not Supported (directly via prost crate)

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)5196
[ +2%]
5.24
[ 4.69 - 5.93]
23.26
[ 22.27 - 24.29]
30.12
[ 27.53 - 37.23]
FlatMessage5196
[ +2%]
5.61
[ 5.09 - 9.37]
32.81
[ 31.98 - 33.72]
39.84
[ 36.56 - 43.92]
postcard (schema)4996
[ -2%]
9.40
[ 8.76 - 13.15]
36.81
[ 34.21 - 38.92]
48.57
[ 43.86 - 50.31]
bincode (schema)5009
[ -1%]
8.77
[ 8.31 - 10.77]
40.38
[ 37.87 - 45.36]
52.20
[ 47.74 - 54.20]
rmp (schema)5075
[ +0%]
8.97
[ 8.28 - 9.84]
47.98
[ 44.86 - 64.15]
60.30
[ 57.04 - 62.03]
rmp5106
[ +1%]
10.37
[ 9.55 - 12.18]
54.21
[ 51.39 - 56.13]
68.88
[ 64.08 - 70.95]
cbor5109
[ +1%]
24.55
[ 22.84 - 26.80]
129.43
[120.49 - 149.67]
156.10
[145.30 - 160.28]
bson5426
[ +7%]
45.29
[ 42.39 - 46.29]
114.96
[111.40 - 120.21]
178.18
[168.74 - 224.70]
simd_json5211
[ +3%]
28.53
[ 27.50 - 30.39]
169.81
[161.40 - 176.34]
204.18
[190.35 - 213.70]
flexbuffers5259
[ +4%]
135.43
[128.13 - 155.44]
93.74
[ 85.96 - 98.08]
240.00
[222.26 - 260.34]
json5211
[ +3%]
130.93
[124.74 - 133.99]
123.41
[118.38 - 135.17]
257.43
[248.58 - 268.54]
toml5216
[ +3%]
703.33
[684.13 - 724.79]
730.39
[694.26 - 742.67]
1465.12
[1394.57 - 1499.54]
protobuf----

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)5196
[ +2%]
12.84
[ 8.94 - 13.48]
16.29
[ 14.69 - 16.81]
24.15
[ 22.66 - 24.94]
FlatMessage5196
[ +2%]
13.48
[ 9.35 - 13.68]
25.64
[ 24.55 - 26.46]
33.67
[ 32.72 - 34.72]
postcard (schema)4996
[ -2%]
10.72
[ 10.51 - 11.08]
27.78
[ 27.40 - 29.04]
38.40
[ 38.16 - 40.01]
bincode (schema)5009
[ -1%]
9.96
[ 9.90 - 10.03]
30.25
[ 30.03 - 30.73]
40.54
[ 40.37 - 41.28]
rmp (schema)5075
[ +0%]
10.40
[ 10.39 - 10.58]
37.87
[ 37.47 - 38.66]
48.61
[ 47.58 - 68.96]
rmp5106
[ +1%]
11.52
[ 11.50 - 12.02]
42.37
[ 41.83 - 43.24]
54.39
[ 53.97 - 55.11]
cbor5109
[ +1%]
22.10
[ 22.06 - 22.58]
97.05
[ 96.02 - 102.05]
120.43
[119.48 - 124.76]
bson5426
[ +7%]
38.79
[ 38.68 - 39.01]
82.71
[ 81.96 - 101.91]
124.26
[123.67 - 126.39]
simd_json5211
[ +3%]
38.11
[ 38.03 - 38.91]
120.43
[118.82 - 123.70]
158.85
[158.38 - 163.32]
flexbuffers5259
[ +4%]
97.97
[ 96.15 - 101.65]
66.20
[ 65.83 - 66.95]
167.95
[166.90 - 174.09]
json5211
[ +3%]
100.08
[100.01 - 102.47]
85.69
[ 85.15 - 101.58]
185.64
[185.34 - 190.29]
toml5216
[ +3%]
778.50
[774.68 - 803.87]
696.37
[690.99 - 736.16]
1489.91
[1479.77 - 1747.68]
protobuf----

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)5196
[ +2%]
6.24
[ 4.82 - 6.75]
13.20
[ 10.72 - 19.85]
19.80
[ 16.45 - 32.59]
FlatMessage5196
[ +2%]
5.03
[ 4.58 - 6.67]
22.95
[ 18.64 - 26.22]
28.88
[ 24.86 - 32.44]
postcard (schema)4996
[ -2%]
8.70
[ 8.02 - 14.57]
26.86
[ 25.57 - 44.28]
36.57
[ 34.88 - 53.06]
bincode (schema)5009
[ -1%]
9.21
[ 8.75 - 12.83]
36.15
[ 31.15 - 42.31]
48.27
[ 42.28 - 69.02]
rmp (schema)5075
[ +0%]
8.18
[ 7.70 - 15.05]
41.17
[ 38.08 - 66.74]
52.07
[ 48.35 - 66.61]
rmp5106
[ +1%]
9.28
[ 8.91 - 11.28]
47.16
[ 44.65 - 50.59]
59.07
[ 56.07 - 85.17]
cbor5109
[ +1%]
24.83
[ 22.68 - 27.00]
114.13
[108.59 - 123.74]
141.75
[135.40 - 147.29]
bson5426
[ +7%]
48.16
[ 44.99 - 57.07]
103.61
[ 98.11 - 135.86]
158.63
[151.21 - 208.65]
simd_json5211
[ +3%]
27.01
[ 25.51 - 28.63]
145.97
[138.55 - 148.97]
180.56
[171.26 - 204.58]
flexbuffers5259
[ +4%]
118.13
[114.57 - 175.30]
82.46
[ 79.07 - 111.77]
212.49
[203.93 - 226.69]
json5211
[ +3%]
138.60
[133.35 - 150.30]
102.57
[ 98.12 - 112.70]
240.33
[231.69 - 285.45]
toml5216
[ +3%]
580.61
[574.53 - 625.95]
634.01
[613.55 - 684.37]
1248.94
[1196.76 - 1293.67]
protobuf----

Process Create Event

This benchmarks simulates a process create event (emitted by Windows Event Log). The idea is to see how well algorithms handle a real world scenario. The size of the event is 233 bytes (by setting up different strings and values).

#![allow(unused)]
fn main() {
pub struct ProcessCreated {
    name: String,
    pid: u32,
    parent_pid: u32,
    parent: String,
    user: String,
    command_line: String,
    timestamp: u32,
    unique_id: u32,
    memory_usage: u64,
    protected_process: bool,
}
}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 100000
  • Data size: 233 bytes
  • Protobuf: Supported

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)298
[ +27%]
2.53
[ 2.06 - 3.01]
17.06
[ 15.59 - 23.34]
20.32
[ 18.00 - 26.85]
FlatMessage298
[ +27%]
2.75
[ 2.33 - 2.95]
20.48
[ 18.86 - 26.08]
22.84
[ 21.59 - 28.98]
bincode (schema)234
[ +0%]
3.50
[ 3.38 - 4.51]
20.86
[ 20.23 - 26.15]
26.65
[ 24.19 - 32.55]
postcard (schema)230
[ -2%]
5.39
[ 4.32 - 5.99]
23.17
[ 19.36 - 26.94]
29.71
[ 25.19 - 34.40]
rmp (schema)238
[ +2%]
4.47
[ 4.00 - 6.65]
26.44
[ 23.71 - 31.10]
30.58
[ 29.34 - 39.52]
protobuf (schema)240
[ +3%]
7.59
[ 5.62 - 8.23]
28.76
[ 24.06 - 33.60]
37.73
[ 33.30 - 44.25]
rmp334
[ +43%]
6.56
[ 5.54 - 9.08]
39.26
[ 38.51 - 49.63]
48.26
[ 47.28 - 60.12]
bson376
[ +61%]
19.40
[ 16.17 - 21.45]
67.24
[ 54.95 - 74.92]
88.09
[ 74.82 - 99.26]
cbor334
[ +43%]
16.38
[ 13.99 - 18.86]
80.14
[ 68.09 - 96.12]
98.78
[ 83.80 - 115.44]
json402
[ +72%]
29.18
[ 24.55 - 34.15]
118.07
[ 93.40 - 126.61]
156.74
[125.77 - 168.44]
simd_json402
[ +72%]
33.36
[ 26.10 - 37.58]
126.90
[108.99 - 147.91]
169.84
[144.93 - 197.78]
flexbuffers453
[ +94%]
134.07
[120.30 - 168.56]
66.81
[ 60.73 - 84.08]
215.41
[191.04 - 268.91]
toml385
[ +65%]
154.82
[129.26 - 181.64]
317.42
[271.30 - 359.40]
506.82
[442.59 - 574.69]

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)298
[ +27%]
2.45
[ 2.42 - 2.56]
9.31
[ 9.12 - 9.61]
11.86
[ 11.77 - 12.46]
FlatMessage298
[ +27%]
2.52
[ 2.44 - 2.60]
11.95
[ 11.85 - 12.48]
14.83
[ 14.49 - 15.05]
bincode (schema)234
[ +0%]
3.83
[ 3.76 - 4.02]
11.87
[ 11.79 - 12.32]
15.77
[ 15.65 - 16.19]
postcard (schema)230
[ -2%]
5.34
[ 5.31 - 5.53]
13.81
[ 13.65 - 14.43]
19.20
[ 19.12 - 20.62]
rmp (schema)238
[ +2%]
4.33
[ 4.30 - 4.81]
16.04
[ 15.89 - 18.12]
20.41
[ 20.30 - 20.86]
protobuf (schema)240
[ +3%]
6.58
[ 6.50 - 7.18]
17.12
[ 16.95 - 17.42]
23.50
[ 23.30 - 23.80]
rmp334
[ +43%]
6.40
[ 6.38 - 6.47]
27.50
[ 27.30 - 27.77]
33.84
[ 33.66 - 34.55]
bson376
[ +61%]
17.01
[ 16.91 - 17.28]
42.29
[ 42.12 - 42.55]
59.32
[ 59.06 - 59.65]
cbor334
[ +43%]
13.65
[ 13.62 - 13.70]
64.24
[ 64.04 - 64.67]
78.41
[ 78.06 - 81.76]
json402
[ +72%]
29.79
[ 29.73 - 29.90]
61.86
[ 61.52 - 62.38]
91.77
[ 91.13 - 94.74]
simd_json402
[ +72%]
30.43
[ 30.33 - 31.44]
90.45
[ 89.90 - 94.72]
124.31
[122.48 - 128.54]
flexbuffers453
[ +94%]
84.15
[ 83.71 - 85.45]
48.21
[ 47.58 - 48.85]
134.98
[134.00 - 137.54]
toml385
[ +65%]
117.64
[115.60 - 120.67]
199.51
[198.30 - 243.70]
325.06
[323.88 - 332.56]

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)298
[ +27%]
1.89
[ 1.87 - 3.12]
5.94
[ 5.78 - 9.80]
8.15
[ 8.05 - 11.84]
FlatMessage298
[ +27%]
1.91
[ 1.71 - 2.48]
9.75
[ 9.28 - 13.14]
12.16
[ 11.86 - 17.74]
postcard (schema)230
[ -2%]
4.34
[ 4.20 - 4.41]
11.18
[ 10.92 - 11.34]
14.90
[ 14.70 - 15.95]
bincode (schema)234
[ +0%]
3.50
[ 3.29 - 4.77]
12.08
[ 11.61 - 20.70]
15.75
[ 15.23 - 28.67]
rmp (schema)238
[ +2%]
3.66
[ 3.54 - 5.76]
14.15
[ 13.61 - 22.21]
18.64
[ 17.89 - 29.29]
protobuf (schema)240
[ +3%]
5.17
[ 4.97 - 5.35]
15.62
[ 15.23 - 21.73]
20.84
[ 20.50 - 30.03]
rmp334
[ +43%]
4.65
[ 4.51 - 6.99]
27.44
[ 26.77 - 35.82]
33.97
[ 32.88 - 36.01]
bson376
[ +61%]
16.35
[ 16.25 - 16.51]
44.33
[ 43.72 - 47.85]
65.05
[ 64.64 - 71.13]
cbor334
[ +43%]
14.22
[ 14.00 - 14.34]
62.14
[ 61.48 - 62.66]
79.70
[ 79.01 - 81.40]
json402
[ +72%]
29.82
[ 29.49 - 31.11]
67.72
[ 66.33 - 72.12]
102.07
[100.37 - 106.50]
simd_json402
[ +72%]
26.07
[ 25.57 - 27.85]
75.24
[ 73.56 - 80.16]
110.27
[108.32 - 113.10]
flexbuffers453
[ +94%]
101.88
[ 98.59 - 132.70]
53.95
[ 53.63 - 83.60]
172.32
[168.33 - 187.01]
toml385
[ +65%]
123.92
[121.40 - 126.69]
245.35
[244.16 - 272.99]
402.49
[386.93 - 444.40]

Nested Structures

This benchmarks compares the performance of the different algorithms to serialize and deserialize a nested structure.

#![allow(unused)]
fn main() {
pub struct LevelOne {
    s1: String,
    s2: String,
    v1: u32,
    v2: u64,
    arr: Vec<u32>,
}

pub struct DepthTwo {
    name: String,
    arr: Vec<String>,
    level_1: Option<LevelOne>,    
}

pub struct NestedStrucs {
    name: String,
    protected_process: bool,
    protected_process: bool,
    level_1: Option<LevelOne>,
    level_2: Option<DepthTwo>,
}

}

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 100000
  • Data size: 413 bytes
  • Protobuf: Supported

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)492
[ +19%]
5.20
[ 4.88 - 5.45]
72.86
[ 70.06 - 75.31]
79.79
[ 76.33 - 82.42]
FlatMessage492
[ +19%]
5.44
[ 5.12 - 6.01]
77.29
[ 74.28 - 78.73]
83.24
[ 79.34 - 84.23]
postcard (schema)363
[ -13%]
12.65
[ 11.78 - 13.56]
78.92
[ 74.58 - 87.94]
92.35
[ 87.00 - 97.69]
bincode (schema)367
[ -12%]
10.33
[ 9.64 - 10.96]
81.51
[ 76.76 - 83.54]
92.47
[ 88.25 - 95.42]
rmp (schema)374
[ -10%]
10.91
[ 10.37 - 11.46]
93.90
[ 89.82 - 97.09]
114.19
[106.86 - 119.30]
protobuf (schema)382
[ -8%]
16.35
[ 15.90 - 18.39]
115.21
[109.60 - 118.23]
136.54
[130.97 - 150.26]
rmp462
[ +11%]
13.39
[ 12.67 - 13.72]
114.01
[108.96 - 117.29]
136.85
[129.60 - 140.75]
json550
[ +33%]
42.16
[ 40.85 - 44.91]
183.08
[175.98 - 201.92]
232.73
[225.25 - 252.79]
cbor463
[ +12%]
34.32
[ 32.90 - 35.54]
214.70
[204.56 - 223.01]
254.37
[246.49 - 262.38]
simd_json550
[ +33%]
37.78
[ 36.30 - 68.77]
208.22
[197.99 - 212.99]
260.11
[248.91 - 262.04]
bson701
[ +69%]
67.60
[ 64.28 - 70.99]
207.89
[195.47 - 214.32]
293.14
[284.69 - 301.15]
flexbuffers561
[ +35%]
236.30
[220.19 - 248.87]
188.59
[182.44 - 194.91]
442.59
[421.71 - 458.80]
toml568
[ +37%]
443.99
[428.89 - 507.61]
790.28
[761.49 - 813.41]
1284.78
[1249.50 - 1332.13]

2. MacOs Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)492
[ +19%]
7.10
[ 7.08 - 7.15]
32.95
[ 32.82 - 33.11]
40.11
[ 40.08 - 40.21]
FlatMessage492
[ +19%]
7.09
[ 7.08 - 10.23]
35.39
[ 35.05 - 36.22]
42.32
[ 42.20 - 63.22]
bincode (schema)367
[ -12%]
10.52
[ 10.44 - 10.55]
37.41
[ 37.29 - 37.44]
48.70
[ 48.40 - 48.84]
postcard (schema)363
[ -13%]
13.09
[ 13.08 - 13.14]
37.52
[ 37.45 - 38.56]
50.81
[ 50.73 - 69.73]
rmp (schema)374
[ -10%]
12.17
[ 12.11 - 12.24]
52.43
[ 52.11 - 53.21]
64.47
[ 64.03 - 64.99]
protobuf (schema)382
[ -8%]
14.73
[ 14.41 - 14.87]
59.14
[ 58.93 - 59.25]
76.91
[ 76.42 - 78.01]
rmp462
[ +11%]
15.60
[ 15.59 - 15.67]
68.62
[ 68.49 - 69.66]
83.70
[ 83.54 - 84.23]
json550
[ +33%]
39.29
[ 39.25 - 39.43]
103.64
[101.90 - 111.56]
143.19
[142.49 - 148.35]
simd_json550
[ +33%]
39.63
[ 39.55 - 40.35]
125.81
[125.50 - 127.23]
168.68
[167.92 - 169.57]
cbor463
[ +12%]
35.39
[ 35.25 - 35.55]
153.95
[153.43 - 155.27]
192.12
[191.47 - 209.82]
bson701
[ +69%]
58.97
[ 58.87 - 59.11]
170.97
[168.88 - 173.12]
237.09
[235.52 - 238.60]
flexbuffers561
[ +35%]
153.11
[152.18 - 154.83]
112.54
[112.17 - 128.58]
274.44
[273.39 - 275.55]
toml568
[ +37%]
320.21
[316.47 - 322.28]
517.88
[514.60 - 519.24]
857.90
[850.86 - 872.40]

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessage (⚠️)492
[ +19%]
3.85
[ 3.75 - 4.08]
22.76
[ 22.11 - 24.03]
28.22
[ 27.52 - 29.62]
FlatMessage492
[ +19%]
3.85
[ 3.48 - 4.21]
25.79
[ 23.51 - 27.38]
31.01
[ 30.04 - 32.83]
postcard (schema)363
[ -13%]
10.40
[ 10.19 - 12.28]
28.75
[ 27.75 - 29.47]
39.97
[ 39.09 - 42.63]
bincode (schema)367
[ -12%]
10.76
[ 10.29 - 15.11]
33.60
[ 31.77 - 51.56]
45.97
[ 44.23 - 54.86]
rmp (schema)374
[ -10%]
9.71
[ 9.40 - 10.91]
42.43
[ 41.54 - 45.29]
55.96
[ 54.28 - 64.53]
protobuf (schema)382
[ -8%]
18.35
[ 17.17 - 20.41]
50.49
[ 48.84 - 61.20]
71.55
[ 70.69 - 105.56]
rmp462
[ +11%]
12.37
[ 11.73 - 14.78]
63.32
[ 61.82 - 66.45]
81.55
[ 78.86 - 94.16]
json550
[ +33%]
43.82
[ 42.84 - 46.60]
103.38
[101.26 - 107.43]
154.22
[151.84 - 187.59]
simd_json550
[ +33%]
39.29
[ 37.64 - 49.09]
118.76
[114.23 - 126.63]
170.38
[162.93 - 175.95]
cbor463
[ +12%]
34.88
[ 33.48 - 40.48]
161.63
[157.12 - 179.40]
198.24
[196.08 - 240.00]
bson701
[ +69%]
67.28
[ 65.60 - 75.67]
144.59
[138.76 - 172.56]
230.37
[220.26 - 268.93]
flexbuffers561
[ +35%]
173.97
[166.69 - 196.07]
128.95
[127.13 - 196.57]
319.99
[307.34 - 391.62]
toml568
[ +37%]
313.47
[294.51 - 352.35]
628.34
[613.12 - 669.64]
1006.52
[960.33 - 1051.88]

Packed vs Struct comparison

FlatMessage supports two types of nested structures, each with different characteristics and compatibility behaviors: FlatMessageStruct and FlatMessagePacked. To test the performance of these two types, we can use the following structure:

#![allow(unused)]
fn main() {
pub struct InnerStruct {
    s1: String,
    s2: String,
    v1: u32,
    v2: u64,
    arr: Vec<u32>,
}

pub struct NestedStruct {
    field: InnerStruct,
}
}

with the following settings (which will be used in the benchmarks):

  1. For FlatMessageStruct:
    #![allow(unused)]
    fn main() {
    #[derive(FlatMessageStruct)]
    pub struct InnerStruct { ... }
    
    #[derive(FlatMessage)]
    pub struct NestedStruct {
        #[flat_message_item(kind = struct, align = 4)]
        field: InnerStruct,
    }
    }
  2. For FlatMessagePacked:
    #![allow(unused)]
    fn main() {
    #[derive(FlatMessagePacked)]
    pub struct InnerStruct { ... }
    
    #[derive(FlatMessage)]
    pub struct NestedStruct {
        #[flat_message_item(kind = packed, align = 4)]
        field: InnerStruct,
    }
    }

Test specs

  • Iterations: k = 10
  • Serialization and deserialization repetitions / iteration: n = 1000000
  • Data size: 161 bytes

Results

1. Windows Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessagePacked (⚠️)185
[ +14%]
17.22
[ 16.57 - 18.34]
129.99
[123.44 - 135.90]
152.09
[144.31 - 159.41]
FlatMessagePacked185
[ +14%]
19.02
[ 18.28 - 21.76]
148.50
[137.33 - 153.57]
169.07
[157.77 - 175.84]
FlatMessageStruct217
[ +34%]
21.34
[ 20.23 - 22.37]
155.35
[145.38 - 163.77]
182.94
[170.53 - 189.36]
FlatMessageStruct (⚠️)217
[ +34%]
21.22
[ 19.83 - 21.58]
158.54
[149.78 - 163.35]
190.52
[180.28 - 194.45]

2. MacOS Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessagePacked (⚠️)185
[ +14%]
15.75
[ 15.34 - 17.77]
68.92
[ 68.41 - 71.38]
86.73
[ 86.27 - 88.73]
FlatMessagePacked185
[ +14%]
15.85
[ 15.31 - 28.30]
87.32
[ 86.91 - 87.82]
104.72
[104.06 - 105.26]
FlatMessageStruct (⚠️)217
[ +34%]
20.39
[ 19.97 - 20.45]
95.73
[ 95.34 - 96.00]
116.05
[113.32 - 116.77]
FlatMessageStruct217
[ +34%]
20.34
[ 20.07 - 20.40]
95.30
[ 94.87 - 95.82]
116.12
[113.39 - 116.94]

3. Linux Execution

AlgorithmSize (b)Ser. (ms)Deser. (ms)Ser+Deser.(ms)
FlatMessagePacked (⚠️)185
[ +14%]
13.65
[ 13.26 - 14.06]
53.51
[ 46.85 - 62.58]
68.61
[ 61.87 - 104.00]
FlatMessagePacked185
[ +14%]
16.54
[ 15.89 - 26.07]
68.38
[ 63.62 - 89.71]
84.28
[ 80.95 - 89.62]
FlatMessageStruct217
[ +34%]
18.29
[ 17.95 - 29.24]
78.46
[ 73.16 - 82.87]
104.51
[ 97.11 - 134.95]
FlatMessageStruct (⚠️)217
[ +34%]
21.69
[ 21.09 - 31.37]
80.87
[ 75.30 - 85.39]
109.66
[102.93 - 123.00]