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)
Option | Type | Default | Description |
---|---|---|---|
store_name | bool | true | Whether to store a hash of the structure name in the serialized data |
version | u8 | 0 | Version number for this structure (1-255). Value 0 means that the structure is not versioned. |
checksum | bool | false | Whether to include CRC32 checksum for data integrity |
validate_name | bool | false | Whether 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_versions | string | none | Version compatibility specification |
optimized_unchecked_code | bool | true | Whether 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 thestore_name
option tofalse
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 tofalse
by default. If set totrue
, 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:
Field Options
Option | Values | Description |
---|---|---|
repr | u8 , i8 , u16 , i16 , u32 , i32 , u64 , i64 | Representation type |
kind | enum , flags , struct , variant | Marks field as enum , flags, variant or a structure type |
align | 1 , 2 , 4 , 8 , 16 | Alignment of the field (only for structures and variants) |
ignore or skip | true or false (default is false) | Ignores the field during serialization and deserialization |
mandatory | true or false (default is true) | Marks the field as mandatory (required) for deserialization |
validate | strict 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. |
default | string | Default 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
orskip
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 theDefault
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 withmandatory = 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).
Option | Default | Description |
---|---|---|
max_size | 16MB | Maximum 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
Type | Zero-Copy | Notes |
---|---|---|
&str | ✅ Yes | Points into original buffer |
&[T] | ✅ Yes | Points into original buffer |
&[u8; N] | ✅ Yes | Points into original buffer |
String | ❌ No | Requires allocation and copy |
Vec<T> | ❌ No | Requires allocation and copy |
Option<&str> | ✅ Yes | When Some, points into buffer |
Option<String> | ❌ No | When 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
andString
) - 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 header5 bytes
xn
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 (wherex
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 header5 bytes
x2
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 areu32
, 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
- Use Storage for serialization: Storage is the only supported serialization target, optimized for FlatMessage workloads
- Prefer zero-copy types: Use
&str
overString
,&[T]
overVec<T>
when possible - Validate when needed: Use
deserialize_from()
for untrusted data,deserialize_from_unchecked()
for performance-critical trusted data - Set appropriate limits: Use Config to prevent excessive memory usage
- 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 Type | Parameters | When It Occurs | Typical Cause | Recovery Strategy |
---|---|---|---|---|
InvalidHeaderLength(usize) | Buffer size | Buffer smaller than minimum header (8 bytes) | Truncated data, wrong format | Check data source, validate input |
InvalidMagic | - | Magic number doesn't match "FLM\x01" | Wrong file format, corruption | Verify file type, check data source |
InvalidSize((u32, u32)) | (actual, expected) | Size in header doesn't match buffer size | Partial read, corruption | Re-read data, validate source |
InvalidOffsetSize | - | Invalid offset size encoding in header | Corruption, unsupported format | Check format version, validate data |
InvalidSizeToStoreMetaData((u32, u32)) | (actual, expected) | Buffer too small for metadata | Incomplete data, corruption | Verify complete transmission |
InvalidHash((u32, u32)) | (actual, expected) | CRC32 hash mismatch | Data corruption, tampering | Re-transmit data, check integrity |
InvalidSizeToStoreFieldsTable((u32, u32)) | (actual, expected) | Buffer too small for field table | Truncated data | Ensure complete data transfer |
IncompatibleVersion(u8) | Version number | Structure version incompatibility | Version mismatch | Migrate data, update code |
FieldIsMissing(u32) | Field hash | Field in data not in struct definition | Schema evolution, wrong struct | Check struct version, migrate |
InvalidFieldOffset((u32, u32)) | (actual, max) | Field offset out of bounds | Corruption, format error | Validate data integrity |
FailToDeserialize(u32) | Field hash | Failed to deserialize specific field | Type mismatch, corruption | Check field compatibility |
NameNotStored | - | Name validation requested but not in data | Missing metadata | Disable validation or add metadata |
UnmatchedName | - | Structure name doesn't match stored name | Wrong struct type | Use correct struct, check data |
ChecksumNotStored | - | Checksum validation requested but not in data | Missing checksum | Disable validation or add checksum |
InvalidChecksum((u32, u32)) | (actual, expected) | Checksum mismatch | Data corruption | Re-transmit, validate source |
ExceedMaxSize((u32, u32)) | (actual, max) | Serialized size exceeds maximum | Data too large, wrong limit | Increase 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
Offset | Name | Type | Observations |
---|---|---|---|
+0 | Magic | (3 bytes) | 'FLM' |
+3 | Format version | u8 | currently value 1 |
+4 | Number of fields | u16 | Total number of fields (data members) in the structure |
+6 | Structure version | Option | For structures that have multiple version, this byte holds the current version of the structure |
+7 | Serializarion flags | u8 | 8 bits that provide information on the data |
+8 | Data | The actual data from the structure | |
+? | Hash table | u32 * Number of fields | A hash table for quick access to the data |
+? | Offset table | ? * Number of fields | A table with indexes from where the data starts |
+? | Timestamp | u64 | Only if the TIMESTAMP flag was set |
+? | Unique ID | u64 | Only if the UNIQUEID flag was set |
+? | Structure Name Hash | u32 | Only if the MAKEHASH flag was set |
+? | Data checksum | u32 | Only 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:
- as a direct value:
#![allow(unused)] fn main() { struct Name { value: T } }
- as a slice of values:
#![allow(unused)] fn main() { struct Name { value: &[T] } }
- as a vector of values:
#![allow(unused)] fn main() { struct Name { value: Vec<T> } }
- 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 avector
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
Boolean values: bool | Yes | Yes | Yes | Yes |
Integer value: u8 , u16 , u32 , u128 , i8 , i16 , i32 , i128 | Yes | Yes | Yes | Yes |
Float values: f32 , f64 | Yes | Yes | Yes | Yes |
Remarks:
- for
bool
values, deserialization usingdeserialize_from
will validate if the value is0
or1
, and will return an error if the value is not valid. If you are certain that the value is valid, you can usedeserialize_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
-
Direct values:
#![allow(unused)] fn main() { use flat_message::*; #[derive(FlatMessage)] struct Example { boolean_value: bool, integer_value: u32, float_value: f64, } }
-
Slices of values:
#![allow(unused)] fn main() { use flat_message::*; #[derive(FlatMessage)] struct Example { boolean_values: &[bool], integer_values: &[u32], float_values: &[f64], } }
-
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>, } }
-
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
String referemce: &str | Yes | - | Yes | Yes |
String object: String | Yes | - | Yes | Yes |
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 usedeserialize_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 aString
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, whileString
is an owned collection of characters that requires allocation and copying of the data.
Example
-
Direct values:
#![allow(unused)] fn main() { use flat_message::*; #[derive(FlatMessage)] struct Example<'a> { string_value: String, str_value: &'a str, } }
-
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>, } }
-
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
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 forIpv6Addr
it is 16 bytes. - The serialization size for
IpAddr
is 5 bybtes (if it is anIpv4Addr
) or 17 bytes (if it is anIpv6Addr
).
Example
-
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, } }
-
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
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 typeUniqueID
:#![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:
Method | Purpose |
---|---|
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
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 typeTimestamp
:#![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 likeCopy
,Clone
,Debug
,Eq
,PartialEq
,Ord
, andPartialOrd
, making it suitable for comparisons and sorting operations.
Methods
The following methods are available for a Timestamp:
Method | Purpose |
---|---|
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
Custom enums with #[derive(FlatMessageEnum)] and #[repr(primitive)] | Yes | Yes | Yes | Yes |
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 usedeserialize_from_unchecked
to skip validation and improve performance.
Example
-
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, } }
-
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], } }
-
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>, } }
-
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, } }
-
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
Custom flags with #[derive(FlatMessageFlags)] and #[repr(transparent)] | Yes | Yes | Yes | Yes |
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 usedeserialize_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 flagsto_value(&self)
- Returns the raw underlying valueany_set(&self, flag: Self)
- Checks if any of the specified flags are setall_set(&self, flag: Self)
- Checks if all of the specified flags are setis_empty(&self)
- Checks if no flags are setset(&mut self, flag: Self)
- Sets the specified flagsunset(&mut self, flag: Self)
- Unsets the specified flagstoggle(&mut self, flag: Self)
- Toggles the specified flagsclear(&mut self)
- Clears all flags
Flags also support standard bitwise operations: |
(OR), &
(AND), ^
(XOR), and their assignment variants (|=
, &=
, ^=
).
Example
-
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, } }
-
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)); }
-
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], } }
-
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>, } }
-
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, } }
-
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
Fixed-size byte array: [u8; N] | Yes | Yes | Yes | Yes |
Reference to fixed-size byte array: &[u8; N] | Yes | - | - | Yes |
Remarks:
- Fixed-size byte arrays (
[u8; N]
) have their size known at compile time, whereN
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
-
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], } }
-
References to fixed-size arrays:
#![allow(unused)] fn main() { use flat_message::*; #[derive(FlatMessage)] struct Example<'a> { buffer_ref: &'a [u8; 10], } }
-
Slices of fixed-size arrays:
#![allow(unused)] fn main() { use flat_message::*; #[derive(FlatMessage)] struct Example<'a> { multiple_buffers: &'a [[u8; 3]], } }
-
Vectors of fixed-size arrays:
#![allow(unused)] fn main() { use flat_message::*; #[derive(FlatMessage)] struct Example { buffer_collection: Vec<[u8; 8]>, } }
-
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
orsha512
)
Structures
Custom structs with explicit alignment are supported for serialization and deserialization.
Data Type | Object | Slice | Vector | Option |
---|---|---|---|---|
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
andUniqueID
. 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
-
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, } }
-
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, } }
-
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, } }
-
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, } }
-
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>, } }
-
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>, } }
-
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:
-
Field Ordering: Fields are reordered during serialization based on their alignment requirements (largest alignment first) to optimize memory layout.
-
Hash Table: Each struct maintains a hash table of its fields for efficient deserialization and version compatibility.
-
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
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
orUniqueID
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
Aspect | Packed Structs | Regular Structs | Observation |
---|---|---|---|
Serialization Speed | Very Fast | Moderate | Sequential writes, no hash tables |
Deserialization Speed | Very Fast | Fast | Sequential reads, no field lookups |
Memory Overhead | Minimal | Moderate | Only 4-byte hash vs hash tables |
Cache Performance | Excellent | Good | Sequential access pattern |
Version Flexibility | Limited | High | Strict hash matching required |
Random Field Access | Not Supported | Fast | Must 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
Feature | Packed Structs | Regular Structs |
---|---|---|
Derive macro | #[derive(FlatMessagePacked)] | #[derive(FlatMessageStruct)] |
Field attribute | kind = packed | kind = 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) |
Overhead | Minimal (4 bytes) | Moderate (hash tables) |
Version compatibility | Strict hash match | Hash-based field lookup |
Performance | Excellent | Good |
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 Type | Object | Slice | Vector | Option |
---|---|---|---|---|
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 usedeserialize_from_unchecked
to skip validation and improve performance
Examples
-
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, } }
-
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, } }
-
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, } }
-
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, } }
-
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, } }
-
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, } }
-
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, } }
-
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
- Version from the start: Always include
version = 1
on new structures - Plan compatibility strategy: Decide upfront whether you need backward, forward, or bidirectional compatibility
- Use optional fields for additions: New fields should be optional (
mandatory = false
) to maintain backward compatibility - Test all compatibility scenarios: Include tests for all supported version combinations
- Understand the two-phase validation: Version check happens before field validation
- Document breaking changes: Clearly mark when mandatory fields are added
- Use version introspection: Check versions before deserialization in multi-version systems
- Plan deprecation cycles: Allow time for systems to upgrade before removing compatibility
Common Pitfalls
- Assuming version compatibility handles fields:
compatible_versions
only controls version acceptance, not field compatibility - Adding mandatory fields to backward-compatible versions: This breaks compatibility even with version ranges
- Not understanding Option
default behavior :Option<T>
fields are automatically optional unless explicitly marked withmandatory = true
- 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:
Type | Default Value | Notes |
---|---|---|
u8 , u16 , u32 , u64 | 0 | Numeric types default to zero |
i8 , i16 , i32 , i64 | 0 | Signed types also default to zero |
f32 , f64 | 0.0 | Floating point defaults to zero |
bool | false | Boolean defaults to false |
String | String::new() | Empty (non allocated) String object |
&str | "" | Empty string (lifetime permitting) |
Vec<T> | [] | Empty vector |
Option<T> | None | Option 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 (useNone
if missing)Option<T>
fields withmandatory = true
: Mandatory (causeError::FieldIsMissing
if not present)Option<T>
fields withmandatory = false
: Optional (useNone
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:
- Version check: Is the data version in
compatible_versions
?- If not →
Error::IncompatibleVersion(version)
- If not →
- Field validation: Are all mandatory fields present?
- If not →
Error::FieldIsMissing(hash)
- If not →
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
- Default to mandatory: Make fields mandatory unless you specifically need them optional
- Plan for evolution: New fields should be optional to maintain backward compatibility
- Leverage Option
for new fields :Option<T>
fields are automatically optional, making them ideal for version evolution - Use meaningful defaults: Provide sensible default values that won't break application logic
- Test compatibility: Always test deserialization across supported version combinations
- 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 deserializedvalidate = 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:
- Normal Operation: If the field can be deserialized normally, it uses the stored value
- 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 toNone
- Uses the
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:
- Security-critical: Cryptographic algorithms, security levels
- Stable protocols: Fixed command sets that shouldn't change
- Data integrity: Any modification should break compatibility
- 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:
- Expected evolution: Types will grow over time
- Forward compatibility: Older code should handle newer types
- API evolution: Public interfaces that might expand
- 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)
mandatory | validate | Result | Behavior |
---|---|---|---|
true/false | strict | FAIL | Type validation fails, deserialization error |
true/false | fallback | SUCCESS | Type 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)
mandatory | validate | Result | Behavior |
---|---|---|---|
true | strict/fallback | FAIL | Required field missing, deserialization error |
false | strict/fallback | SUCCESS | Optional 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
- Avoid changing primitive types - they always break compatibility
- Keep representation consistent for complex types when possible
- Use
mandatory = false
for fields that might be removed or changed - Use
validate = fallback
to gracefully handle type mismatches
For Forward Compatibility
- Plan representation carefully - changing
repr
breaks compatibility - Consider using
Option<T>
to make fields truly optional - Document default values clearly for fallback scenarios
Migration Strategies
- Gradual migration: Introduce new field, deprecate old field
- Representation preservation: Keep same
repr
when changing complex types - 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 withstrict
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:
FlatMessageStruct
- Hash-based structures with flexible compatibilityFlatMessagePacked
- 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
andUniqueID
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
andvalidate
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
orUniqueID
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
-
Use Optional Fields: Mark new fields as
mandatory = false
#![allow(unused)] fn main() { #[flat_message_item(mandatory = false, default = 42)] pub new_field: u16, }
-
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, }
-
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
-
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, }
-
Design for Immutability: Treat packed structs as immutable once deployed
-
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
Type | Serialization | Deserialization | Memory 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 As | Deserialized As | Result |
---|---|---|
Some(value) | T | ✅ Works, gets value |
None | T | ❌ Error: field missing |
T | Option<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
Serialize | Deserialize | Compatible | Performance | Notes |
---|---|---|---|---|
Vec<T> | Vec<T> | ✅ | Slow | Copy required |
Vec<T> | &[T] | ✅ | Fast | Zero-copy |
&[T] | Vec<T> | ✅ | Slow | Copy required |
&[T] | &[T] | ✅ | Fast | Zero-copy |
Some(T) | T | ✅ | Same as T | Direct access |
None | T | ❌ | - | Error: field missing |
T | Option<T> | ✅ | Same as T | Wrapped in Some |
String | &str | ✅ | Fast | Zero-copy |
&str | String | ✅ | Slow | Copy required |
Default Values
There are several scenarios where a default value will be used to initialize a field during deserialization:
- The field is not mandatory and is not present in the serialized data.
- The field is present in the serialized data, but it has some issues trying to deserialize it and the
validate
attribute is set tofallback
. - The field is skipped during serialization and it has to be defaulted during deserialization.
In these scenarios, FlatMessage will do one of the following:
- Use the default value for the type if it is available (this implies that the type implements the
Default
trait). - If the attribute
default
is specified, it will use the value of the attribute.
Example:
- Use the default value:
In this case, the field#![allow(unused)] fn main() { #[derive(FlatMessage)] struct Test { #[flat_message_item(skip = true)] a: u32, } }
a
will be initialized to0
(the default value foru32
). - Use the value of the attribute
default
:
In this case, the field#![allow(unused)] fn main() { #[derive(FlatMessage)] struct Test { #[flat_message_item(skip = true, default = 10)] a: u32, } }
a
will be initialized to10
.
Custom Default Values
When using the attribute default
, you can specify a custom default value for the field in the following ways:
- A constant value (e.g.
default = 10
ordefault = MY_CONSTANT
). - 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. - 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:
- if the type is
&str
the value of the default attribute is kept as it is. - if the type is
String
th value of the default attribute is converted into a String - if the type is NOT a string the quotes (
"
) are removed and the actual value will be used - if the type is on option (
Option<T>
) then:- if the value of the default attribute is
None
then the field is set toNone
- if the value of the default attribute is
Some(T)
then the field is set toSome(T)
- otherwise the value of the default attribute is converted into a
Some<value>
- if the value of the default attribute is
Examples
Type | Default value | Actual value |
---|---|---|
Numeric (u8, u32, f32, etc) | default = "10" | 10 |
Numeric (u8, u32, f32, etc) | default = 123 | 123 |
Numeric (u8, u32, f32, etc) | default = MY_CONSTANT | MY_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 = false | false |
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) |
Option | default = "None" | None |
Option | default = "Some(123)" | Some(123) |
Option | default = MY_CONSTANT | MY_CONSTANT (it is assumed that MY_CONSTANT exists in the current scope and it of type Option<T> ) |
Option | default = r#"foo(1+2+3)"# | foo(1+2+3) (it is assumed that foo exists in the current scope and returns an Option<T> ) |
Option | default = "4" | Some(4) (the value is automatically converted into a Some<T> ) |
Option<&str> | default = "Hello" | Some("Hello") |
Option | default = "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 validatednever
--> checksum is ignoredauto
--> 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
Mode | Checksum Present | Checksum Missing | Behavior |
---|---|---|---|
"always" | ✅ Validates | ❌ Error | Always requires checksum |
"never" | ⚡ Skips | ✅ Continues | Never validates |
"auto" | ✅ Validates | ✅ Continues | Validates 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 datavalidate_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
-
Use strict validation (
store_name = true, validate_name = true
) for:- Network protocol messages
- API endpoints
- Critical data structures where type safety is paramount
-
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
-
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
-
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:
- Speed - how fast is the serialization/deserialization process is compared to other serializers/deserializers
- 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:
- Safe vs Unsafe deserialization (and their performance implications)
- Zero-copy deserialization (and its performance implications)
- 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 / method | Version | Schema Type | Observation |
---|---|---|---|
flat_message | 0.1.0 | Schema-less | For deserialization the deserialize(...) method is beng used |
flat_message (⚠️) | 0.1.0 | Schema-less | (Unchecked) For deserialization the deserialize_unchecked(...) method is beng used (meaning that no validation is done) |
bincode | 2.0.1 | with Schema | also use bincode_derive (2.0.1) |
bson | 3.0.0 | Schema-less | |
flexbuffers | 25.2.10 | Schema-less | |
postcard | 1.1.3 | with Schema | |
serde_json | 1.0.143 | Schema-less | |
simd_json | 0.15.1 | Schema-less | |
ciborium | 0.2.2 | Schema-less | |
rmp | 0.8.14 | both | also included rmp-serde for MessagePack (v1.3.0) |
toml | 0.9.5 | Schema-less | TOML 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.1 | with Schema | Protobuf 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 forn
times (repetitions) and measure the time needed to perform this operationsDeser Time
- Deserialize a buffer containing the serialized data forn
times (repetitions) and measure the time needed to perform this operationsSer+Deser Time
- Serialize and then deserialize the structure forn
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:
- Windows - Windows 11, 64 bit,11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz (2.80 GHz), RAM 32.0 GB
- MacOS - MacOS 15.6.1 24G90 arm64, Apple M1 Pro, RAM 32.0 GB
- 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.
Algorithm | Win (MB/sec) | Mac (MB/sec) | Linux (MB/sec) |
---|---|---|---|
FlatMessage (⚠️) | 4624.31 | 5143.91 | 6705.44 |
FlatMessage | 3888.78 | 4157.94 | 5072.87 |
protobuf (schema) | 2261.02 | 2357.24 | 2798.58 |
postcard (schema) | 2212.56 | 2726.47 | 2959.57 |
bincode (schema) | 2024.51 | 2478.93 | 2323.05 |
rmp (schema) | 1814.16 | 2110.85 | 2345.71 |
rmp | 1468.29 | 1721.22 | 1796.20 |
bson | 850.00 | 1089.00 | 1025.31 |
cbor | 756.17 | 860.30 | 853.52 |
flexbuffers | 410.41 | 582.94 | 494.43 |
simd_json | 377.15 | 498.02 | 464.32 |
json | 341.76 | 479.47 | 391.95 |
toml | 63.20 | 70.70 | 73.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
andi16
that are not supported by protobuf)
Results
1. Windows Execution
Algorithm | Size (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] |
FlatMessage | 355 [ +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] |
rmp | 776 [+269%] | 21.74 [ 18.31 - 22.99] | 90.83 [ 78.58 - 99.07] | 126.98 [112.18 - 138.83] |
cbor | 786 [+274%] | 47.90 [ 42.23 - 55.22] | 169.09 [148.57 - 182.64] | 222.42 [192.96 - 240.77] |
json | 895 [+326%] | 68.24 [ 58.92 - 74.31] | 140.85 [122.75 - 151.39] | 225.94 [198.50 - 246.99] |
bson | 885 [+321%] | 57.16 [ 51.87 - 62.13] | 164.07 [144.80 - 178.24] | 233.80 [204.84 - 259.61] |
simd_json | 895 [+326%] | 88.45 [ 76.05 - 99.23] | 168.27 [151.13 - 181.42] | 270.51 [248.37 - 295.02] |
flexbuffers | 1022 [+386%] | 439.06 [381.02 - 476.33] | 181.15 [165.53 - 196.50] | 646.95 [576.51 - 696.58] |
toml | 894 [+325%] | 369.89 [335.68 - 402.81] | 856.84 [754.40 - 938.28] | 1254.35 [1127.43 - 1386.98] |
protobuf | - | - | - | - |
2. MacOs Execution
Algorithm | Size (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] |
FlatMessage | 355 [ +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] |
rmp | 776 [+269%] | 21.10 [ 21.10 - 21.50] | 58.73 [ 58.55 - 60.41] | 80.55 [ 80.29 - 82.82] |
json | 895 [+326%] | 59.76 [ 59.59 - 61.49] | 81.90 [ 81.72 - 84.49] | 142.89 [142.56 - 147.54] |
bson | 885 [+321%] | 51.69 [ 51.63 - 52.08] | 116.45 [116.16 - 117.56] | 169.88 [169.48 - 172.46] |
simd_json | 895 [+326%] | 63.60 [ 63.40 - 65.63] | 107.18 [106.12 - 110.47] | 173.02 [171.21 - 177.92] |
cbor | 786 [+274%] | 42.49 [ 42.45 - 43.61] | 132.44 [132.25 - 132.82] | 176.32 [175.69 - 176.87] |
flexbuffers | 1022 [+386%] | 349.96 [348.16 - 359.48] | 112.39 [111.88 - 119.31] | 467.53 [466.12 - 480.27] |
toml | 894 [+325%] | 288.15 [283.89 - 295.04] | 554.20 [552.77 - 557.26] | 857.29 [855.34 - 864.52] |
protobuf | - | - | - | - |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 355 [ +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] |
rmp | 776 [+269%] | 21.56 [ 20.76 - 23.37] | 69.45 [ 68.10 - 75.75] | 95.85 [ 93.59 - 98.88] |
json | 895 [+326%] | 64.40 [ 62.01 - 69.40] | 113.40 [109.06 - 127.41] | 197.48 [184.91 - 247.76] |
simd_json | 895 [+326%] | 67.63 [ 62.81 - 74.22] | 125.92 [117.82 - 146.19] | 205.14 [193.67 - 269.61] |
cbor | 786 [+274%] | 46.78 [ 45.37 - 48.92] | 151.96 [147.25 - 155.70] | 207.72 [199.29 - 233.28] |
bson | 885 [+321%] | 54.75 [ 51.70 - 78.50] | 144.14 [139.10 - 160.18] | 208.93 [197.22 - 229.21] |
flexbuffers | 1022 [+386%] | 395.54 [376.59 - 449.20] | 153.44 [150.59 - 165.30] | 579.35 [544.29 - 630.75] |
toml | 894 [+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
Algorithm | Size (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] |
FlatMessage | 26 [+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] |
rmp | 7 [ -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] |
json | 17 [+112%] | 11.30 [ 10.51 - 12.34] | 24.70 [ 22.61 - 25.42] | 39.57 [ 36.79 - 41.07] |
bson | 19 [+137%] | 17.92 [ 17.33 - 28.72] | 43.09 [ 42.27 - 60.24] | 60.15 [ 57.02 - 62.73] |
cbor | 8 [ +0%] | 14.51 [ 13.55 - 17.31] | 56.70 [ 55.18 - 64.55] | 68.12 [ 66.53 - 81.23] |
simd_json | 17 [+112%] | 10.86 [ 10.31 - 11.05] | 191.43 [181.89 - 194.51] | 207.16 [193.80 - 208.84] |
flexbuffers | 17 [+112%] | 189.42 [184.32 - 192.16] | 44.84 [ 40.74 - 46.18] | 251.47 [238.92 - 253.57] |
toml | 16 [+100%] | 157.98 [149.10 - 159.28] | 286.86 [270.90 - 290.13] | 476.62 [452.59 - 489.41] |
2. MacOs Execution
Algorithm | Size (b) | Ser. (ms) | Deser. (ms) | Ser+Deser.(ms) |
---|---|---|---|---|
FlatMessage | 26 [+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] |
rmp | 7 [ -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] |
json | 17 [+112%] | 11.64 [ 11.62 - 11.77] | 20.35 [ 20.16 - 20.54] | 31.77 [ 31.63 - 32.02] |
bson | 19 [+137%] | 12.40 [ 12.38 - 12.48] | 24.88 [ 24.84 - 34.03] | 41.56 [ 41.49 - 41.80] |
cbor | 8 [ +0%] | 13.18 [ 13.17 - 13.28] | 62.83 [ 62.76 - 66.50] | 77.80 [ 77.63 - 79.15] |
flexbuffers | 17 [+112%] | 99.77 [ 99.40 - 100.22] | 29.26 [ 29.22 - 49.78] | 139.39 [138.64 - 153.18] |
simd_json | 17 [+112%] | 8.64 [ 8.59 - 8.91] | 129.03 [125.62 - 136.86] | 139.43 [136.12 - 144.18] |
toml | 16 [+100%] | 81.83 [ 81.68 - 84.24] | 220.94 [219.94 - 231.54] | 325.22 [324.07 - 333.64] |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 26 [+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] |
rmp | 7 [ -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] |
json | 17 [+112%] | 10.68 [ 10.50 - 12.46] | 22.46 [ 22.05 - 23.62] | 34.82 [ 34.23 - 60.41] |
bson | 19 [+137%] | 16.97 [ 16.66 - 17.28] | 42.52 [ 41.85 - 47.99] | 60.10 [ 58.70 - 69.23] |
cbor | 8 [ +0%] | 15.81 [ 15.70 - 16.18] | 59.24 [ 58.39 - 61.95] | 69.63 [ 68.21 - 70.32] |
simd_json | 17 [+112%] | 10.24 [ 9.92 - 10.54] | 112.17 [102.62 - 155.85] | 127.21 [118.65 - 164.30] |
flexbuffers | 17 [+112%] | 110.84 [108.56 - 114.05] | 45.89 [ 45.12 - 71.19] | 173.46 [168.91 - 198.21] |
toml | 16 [+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
Algorithm | Size (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] |
FlatMessage | 3968 [ +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] |
rmp | 3989 [ +1%] | 6.71 [ 6.22 - 9.30] | 45.51 [ 41.83 - 59.64] | 54.31 [ 51.52 - 69.65] |
bson | 4014 [ +2%] | 13.09 [ 12.06 - 17.22] | 54.05 [ 50.04 - 66.43] | 71.57 [ 68.23 - 85.13] |
cbor | 3989 [ +1%] | 11.26 [ 10.88 - 15.10] | 78.63 [ 77.29 - 99.97] | 91.99 [ 88.11 - 120.60] |
flexbuffers | 4041 [ +3%] | 107.08 [101.70 - 128.89] | 66.68 [ 64.52 - 77.82] | 185.35 [180.24 - 219.79] |
simd_json | 4011 [ +2%] | 27.61 [ 25.73 - 28.45] | 207.88 [195.33 - 236.90] | 246.34 [231.25 - 276.01] |
json | 4011 [ +2%] | 179.69 [170.16 - 207.78] | 113.51 [104.95 - 122.49] | 298.94 [283.15 - 316.62] |
toml | 4010 [ +2%] | 821.39 [786.00 - 907.08] | 758.25 [713.60 - 796.55] | 1638.54 [1545.04 - 1743.35] |
2. MacOs Execution
Algorithm | Size (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] |
FlatMessage | 3968 [ +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] |
rmp | 3989 [ +1%] | 8.27 [ 8.26 - 8.39] | 40.59 [ 39.59 - 42.12] | 49.34 [ 48.31 - 50.44] |
bson | 4014 [ +2%] | 13.56 [ 13.52 - 13.59] | 44.21 [ 43.06 - 44.91] | 58.75 [ 57.73 - 59.61] |
cbor | 3989 [ +1%] | 12.84 [ 12.82 - 12.89] | 72.15 [ 71.56 - 73.79] | 86.91 [ 86.36 - 88.37] |
flexbuffers | 4041 [ +3%] | 71.80 [ 67.99 - 72.59] | 53.19 [ 52.43 - 54.70] | 133.62 [132.45 - 135.85] |
simd_json | 4011 [ +2%] | 42.54 [ 42.40 - 43.27] | 147.50 [145.23 - 149.58] | 191.22 [188.72 - 194.15] |
json | 4011 [ +2%] | 139.28 [138.95 - 139.90] | 85.05 [ 84.74 - 87.02] | 225.44 [224.47 - 229.65] |
toml | 4010 [ +2%] | 1064.13 [1059.03 - 1069.45] | 706.01 [702.41 - 722.81] | 1799.29 [1773.97 - 1810.07] |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 3968 [ +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] |
rmp | 3989 [ +1%] | 6.52 [ 6.26 - 6.94] | 36.78 [ 35.60 - 39.13] | 43.45 [ 42.51 - 47.37] |
bson | 4014 [ +2%] | 13.06 [ 12.35 - 18.81] | 42.50 [ 38.38 - 44.33] | 58.51 [ 53.93 - 61.10] |
cbor | 3989 [ +1%] | 11.56 [ 10.97 - 15.93] | 67.95 [ 63.18 - 75.39] | 81.07 [ 75.50 - 118.26] |
flexbuffers | 4041 [ +3%] | 84.05 [ 73.17 - 99.47] | 56.81 [ 51.97 - 78.07] | 149.65 [137.27 - 157.13] |
simd_json | 4011 [ +2%] | 27.65 [ 26.27 - 30.50] | 172.07 [163.45 - 203.11] | 204.12 [196.88 - 219.06] |
json | 4011 [ +2%] | 184.73 [178.11 - 234.89] | 94.42 [ 89.33 - 104.79] | 280.38 [271.74 - 289.52] |
toml | 4010 [ +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
Algorithm | Size (b) | Ser. (ms) | Deser. (ms) | Ser+Deser.(ms) |
---|---|---|---|---|
FlatMessage | 388060 [ +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] |
rmp | 445039 [ +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] |
cbor | 320339 [ -18%] | 49.73 [ 43.10 - 64.13] | 94.20 [ 84.26 - 111.31] | 145.84 [131.47 - 170.92] |
flexbuffers | 264099 [ -32%] | 66.30 [ 58.72 - 98.01] | 110.55 [ 97.93 - 156.83] | 180.88 [157.36 - 243.98] |
json | 539243 [ +38%] | 265.55 [227.27 - 308.44] | 99.88 [ 94.64 - 116.47] | 353.35 [330.89 - 417.55] |
bson | 960615 [+147%] | 190.55 [157.87 - 219.51] | 244.22 [204.61 - 285.33] | 432.94 [371.42 - 504.68] |
simd_json | 539243 [ +38%] | 286.91 [262.48 - 337.45] | 177.50 [167.55 - 199.11] | 482.78 [433.14 - 559.85] |
toml | 606238 [ +56%] | 350.72 [314.07 - 397.58] | 1259.97 [1051.49 - 1489.37] | 1622.38 [1398.75 - 2618.01] |
2. MacOs Execution
Algorithm | Size (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] |
FlatMessage | 388060 [ +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] |
rmp | 445039 [ +14%] | 17.35 [ 17.31 - 20.36] | 33.35 [ 31.54 - 37.02] | 51.29 [ 49.43 - 54.87] |
flexbuffers | 264099 [ -32%] | 60.09 [ 58.93 - 61.57] | 61.73 [ 59.33 - 67.41] | 125.72 [123.06 - 134.17] |
cbor | 320339 [ -18%] | 46.99 [ 46.91 - 48.50] | 81.21 [ 80.76 - 83.73] | 128.18 [127.71 - 153.43] |
bson | 960615 [+147%] | 118.51 [118.05 - 122.29] | 153.30 [149.42 - 157.42] | 274.78 [266.38 - 295.11] |
json | 539243 [ +38%] | 194.88 [193.23 - 231.19] | 82.23 [ 78.61 - 85.68] | 280.23 [272.33 - 286.06] |
simd_json | 539243 [ +38%] | 214.50 [212.87 - 247.29] | 98.83 [ 95.00 - 120.02] | 317.99 [313.92 - 326.43] |
toml | 606238 [ +56%] | 202.20 [200.94 - 213.92] | 672.79 [658.82 - 697.97] | 873.71 [862.31 - 902.96] |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 388060 [ +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] |
rmp | 445039 [ +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] |
cbor | 320339 [ -18%] | 53.78 [ 53.01 - 75.71] | 89.73 [ 87.73 - 121.95] | 145.53 [142.69 - 153.50] |
flexbuffers | 264099 [ -32%] | 58.48 [ 57.33 - 73.69] | 107.12 [106.37 - 142.04] | 165.70 [162.62 - 221.79] |
json | 539243 [ +38%] | 194.15 [191.85 - 200.21] | 95.20 [ 94.02 - 97.09] | 291.55 [284.73 - 343.34] |
simd_json | 539243 [ +38%] | 221.01 [218.67 - 237.59] | 110.15 [106.49 - 192.84] | 333.07 [329.37 - 396.38] |
bson | 960615 [+147%] | 178.64 [172.46 - 186.55] | 218.06 [212.31 - 234.52] | 403.27 [388.82 - 439.16] |
toml | 606238 [ +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
Algorithm | Size (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] |
FlatMessage | 51 [+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] |
rmp | 26 [+100%] | 7.44 [ 5.63 - 8.74] | 46.45 [ 34.02 - 54.01] | 53.70 [ 39.64 - 62.60] |
json | 38 [+192%] | 33.50 [ 24.57 - 37.36] | 78.92 [ 58.62 - 91.34] | 112.82 [ 82.15 - 127.90] |
bson | 45 [+246%] | 42.67 [ 30.45 - 48.13] | 102.22 [ 72.41 - 113.37] | 147.86 [107.70 - 165.23] |
cbor | 26 [+100%] | 29.30 [ 21.86 - 33.01] | 158.47 [116.82 - 182.08] | 185.83 [129.80 - 209.97] |
simd_json | 38 [+192%] | 36.12 [ 24.98 - 49.41] | 330.43 [220.58 - 493.58] | 384.26 [243.10 - 545.29] |
flexbuffers | 44 [+238%] | 383.99 [279.44 - 433.44] | 101.35 [ 75.51 - 112.54] | 545.00 [413.95 - 628.83] |
toml | 37 [+184%] | 469.24 [307.22 - 492.86] | 580.37 [386.81 - 633.13] | 1073.73 [754.72 - 1187.96] |
protobuf | - | - | - | - |
2. MacOs Execution
Algorithm | Size (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] |
FlatMessage | 51 [+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] |
rmp | 26 [+100%] | 4.97 [ 4.76 - 5.11] | 30.33 [ 29.55 - 30.88] | 34.20 [ 33.89 - 35.07] |
json | 38 [+192%] | 24.06 [ 23.90 - 24.74] | 36.92 [ 36.70 - 37.95] | 60.92 [ 60.13 - 62.20] |
bson | 45 [+246%] | 27.89 [ 27.38 - 28.39] | 49.57 [ 49.15 - 51.08] | 81.31 [ 80.06 - 82.29] |
cbor | 26 [+100%] | 17.40 [ 17.26 - 17.79] | 102.34 [101.03 - 104.12] | 121.39 [119.61 - 123.23] |
simd_json | 38 [+192%] | 26.13 [ 25.75 - 26.77] | 193.82 [186.38 - 214.73] | 220.08 [213.59 - 253.01] |
flexbuffers | 44 [+238%] | 161.10 [158.31 - 163.03] | 67.87 [ 66.92 - 69.34] | 234.97 [232.07 - 261.17] |
toml | 37 [+184%] | 195.04 [192.15 - 197.56] | 295.58 [288.06 - 300.80] | 515.11 [499.74 - 539.25] |
protobuf | - | - | - | - |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 51 [+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] |
rmp | 26 [+100%] | 5.35 [ 5.28 - 5.50] | 38.55 [ 37.46 - 40.42] | 45.84 [ 44.45 - 52.82] |
json | 38 [+192%] | 24.12 [ 23.71 - 24.78] | 44.71 [ 43.98 - 46.08] | 70.54 [ 68.76 - 104.62] |
bson | 45 [+246%] | 28.20 [ 27.70 - 29.17] | 77.89 [ 76.43 - 79.92] | 108.27 [107.16 - 112.51] |
cbor | 26 [+100%] | 22.64 [ 22.09 - 23.73] | 118.84 [116.84 - 138.67] | 137.75 [135.90 - 145.47] |
simd_json | 38 [+192%] | 21.20 [ 20.89 - 28.28] | 128.36 [124.29 - 133.47] | 158.68 [153.74 - 184.81] |
flexbuffers | 44 [+238%] | 176.53 [169.48 - 187.20] | 79.01 [ 77.30 - 110.28] | 288.10 [279.43 - 292.04] |
toml | 37 [+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 toSome("Hello, World - this is an option field")
opt_two
is initialized toSome(12345678)
opt_three
is initialized toNone
opt_four
is initialized toSome([1, 2, 3, 4, 100, 200, 300, 400, 1000, 2000, 3000, 4000, 10000, 20000, 30000, 40000])
opt_five
is initialized toSome(["Hello", "World", "This", "is", "an", "option", "field"])
opt_six
is initialized toNone
opt_seven
is initialized toNone
#![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
Algorithm | Size (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] |
FlatMessage | 191 [ +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] |
rmp | 188 [ +29%] | 10.82 [ 9.89 - 12.04] | 72.12 [ 68.27 - 82.56] | 86.56 [ 82.02 - 98.82] |
json | 264 [ +82%] | 23.04 [ 21.89 - 26.13] | 118.43 [110.81 - 135.33] | 149.71 [142.48 - 169.75] |
simd_json | 264 [ +82%] | 26.31 [ 25.21 - 29.14] | 153.30 [149.29 - 205.95] | 188.02 [173.91 - 241.76] |
cbor | 187 [ +28%] | 26.08 [ 24.49 - 30.69] | 161.60 [150.53 - 198.38] | 190.97 [183.57 - 335.13] |
bson | 402 [+177%] | 67.99 [ 63.65 - 86.69] | 152.11 [143.13 - 185.71] | 232.28 [217.78 - 269.26] |
flexbuffers | 254 [ +75%] | 151.98 [145.21 - 176.35] | 116.80 [109.76 - 128.22] | 282.20 [264.99 - 322.41] |
toml | 235 [ +62%] | 157.57 [143.44 - 171.29] | 431.05 [401.97 - 472.36] | 601.73 [564.37 - 655.63] |
protobuf | - | - | - | - |
2. MacOs Execution
Algorithm | Size (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] |
FlatMessage | 191 [ +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] |
rmp | 188 [ +29%] | 11.88 [ 11.72 - 12.11] | 44.46 [ 44.05 - 45.68] | 56.63 [ 56.12 - 56.98] |
json | 264 [ +82%] | 23.77 [ 23.49 - 23.87] | 68.47 [ 67.91 - 68.60] | 92.42 [ 92.15 - 92.85] |
simd_json | 264 [ +82%] | 27.25 [ 26.82 - 27.53] | 104.36 [103.07 - 107.31] | 131.90 [131.54 - 192.58] |
cbor | 187 [ +28%] | 24.08 [ 23.75 - 24.39] | 121.37 [120.49 - 122.90] | 146.94 [145.62 - 147.81] |
bson | 402 [+177%] | 52.94 [ 52.37 - 53.36] | 101.40 [100.55 - 120.19] | 159.05 [158.11 - 160.64] |
flexbuffers | 254 [ +75%] | 107.38 [106.93 - 108.01] | 73.84 [ 73.58 - 74.24] | 186.75 [186.05 - 187.50] |
toml | 235 [ +62%] | 117.48 [116.20 - 118.65] | 282.25 [280.15 - 286.42] | 408.69 [406.48 - 432.76] |
protobuf | - | - | - | - |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 191 [ +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] |
rmp | 188 [ +29%] | 9.28 [ 9.05 - 10.38] | 42.29 [ 41.09 - 47.13] | 55.92 [ 54.69 - 62.79] |
json | 264 [ +82%] | 22.23 [ 21.78 - 26.22] | 73.41 [ 71.95 - 81.41] | 105.77 [ 97.07 - 133.97] |
simd_json | 264 [ +82%] | 26.10 [ 24.60 - 37.97] | 94.13 [ 88.75 - 99.57] | 124.87 [119.99 - 134.12] |
cbor | 187 [ +28%] | 25.62 [ 24.91 - 27.00] | 127.50 [124.79 - 133.87] | 154.28 [152.52 - 165.43] |
bson | 402 [+177%] | 63.22 [ 62.09 - 66.58] | 113.48 [111.71 - 120.82] | 186.09 [183.00 - 244.09] |
flexbuffers | 254 [ +75%] | 118.33 [113.95 - 136.28] | 83.15 [ 82.46 - 92.44] | 230.28 [209.08 - 262.75] |
toml | 235 [ +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 toMyVariant::U32(0x12345)
v2
is initialized toMyVariant::U64(0x1234567890)
v3
is initialized toMyVariant::String(String::from("Hello, World!"))
v4
is initialized toMyVariant::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 toMyVariant::StringVector(vec![String::from("Hello"), String::from("World"), String::from("This"), String::from("is"), String::from("a"), String::from("test")])
v6
is initialized toMyVariant::SimpleVariant
v7
is initialized toMyVariant::U32(0)
v8
is initialized toMyVariant::U64(100)
v9
is initialized toNone
v10
is initialized toSome(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
Algorithm | Size (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] |
FlatMessage | 5196 [ +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] |
rmp | 5106 [ +1%] | 10.37 [ 9.55 - 12.18] | 54.21 [ 51.39 - 56.13] | 68.88 [ 64.08 - 70.95] |
cbor | 5109 [ +1%] | 24.55 [ 22.84 - 26.80] | 129.43 [120.49 - 149.67] | 156.10 [145.30 - 160.28] |
bson | 5426 [ +7%] | 45.29 [ 42.39 - 46.29] | 114.96 [111.40 - 120.21] | 178.18 [168.74 - 224.70] |
simd_json | 5211 [ +3%] | 28.53 [ 27.50 - 30.39] | 169.81 [161.40 - 176.34] | 204.18 [190.35 - 213.70] |
flexbuffers | 5259 [ +4%] | 135.43 [128.13 - 155.44] | 93.74 [ 85.96 - 98.08] | 240.00 [222.26 - 260.34] |
json | 5211 [ +3%] | 130.93 [124.74 - 133.99] | 123.41 [118.38 - 135.17] | 257.43 [248.58 - 268.54] |
toml | 5216 [ +3%] | 703.33 [684.13 - 724.79] | 730.39 [694.26 - 742.67] | 1465.12 [1394.57 - 1499.54] |
protobuf | - | - | - | - |
2. MacOs Execution
Algorithm | Size (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] |
FlatMessage | 5196 [ +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] |
rmp | 5106 [ +1%] | 11.52 [ 11.50 - 12.02] | 42.37 [ 41.83 - 43.24] | 54.39 [ 53.97 - 55.11] |
cbor | 5109 [ +1%] | 22.10 [ 22.06 - 22.58] | 97.05 [ 96.02 - 102.05] | 120.43 [119.48 - 124.76] |
bson | 5426 [ +7%] | 38.79 [ 38.68 - 39.01] | 82.71 [ 81.96 - 101.91] | 124.26 [123.67 - 126.39] |
simd_json | 5211 [ +3%] | 38.11 [ 38.03 - 38.91] | 120.43 [118.82 - 123.70] | 158.85 [158.38 - 163.32] |
flexbuffers | 5259 [ +4%] | 97.97 [ 96.15 - 101.65] | 66.20 [ 65.83 - 66.95] | 167.95 [166.90 - 174.09] |
json | 5211 [ +3%] | 100.08 [100.01 - 102.47] | 85.69 [ 85.15 - 101.58] | 185.64 [185.34 - 190.29] |
toml | 5216 [ +3%] | 778.50 [774.68 - 803.87] | 696.37 [690.99 - 736.16] | 1489.91 [1479.77 - 1747.68] |
protobuf | - | - | - | - |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 5196 [ +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] |
rmp | 5106 [ +1%] | 9.28 [ 8.91 - 11.28] | 47.16 [ 44.65 - 50.59] | 59.07 [ 56.07 - 85.17] |
cbor | 5109 [ +1%] | 24.83 [ 22.68 - 27.00] | 114.13 [108.59 - 123.74] | 141.75 [135.40 - 147.29] |
bson | 5426 [ +7%] | 48.16 [ 44.99 - 57.07] | 103.61 [ 98.11 - 135.86] | 158.63 [151.21 - 208.65] |
simd_json | 5211 [ +3%] | 27.01 [ 25.51 - 28.63] | 145.97 [138.55 - 148.97] | 180.56 [171.26 - 204.58] |
flexbuffers | 5259 [ +4%] | 118.13 [114.57 - 175.30] | 82.46 [ 79.07 - 111.77] | 212.49 [203.93 - 226.69] |
json | 5211 [ +3%] | 138.60 [133.35 - 150.30] | 102.57 [ 98.12 - 112.70] | 240.33 [231.69 - 285.45] |
toml | 5216 [ +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
Algorithm | Size (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] |
FlatMessage | 298 [ +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] |
rmp | 334 [ +43%] | 6.56 [ 5.54 - 9.08] | 39.26 [ 38.51 - 49.63] | 48.26 [ 47.28 - 60.12] |
bson | 376 [ +61%] | 19.40 [ 16.17 - 21.45] | 67.24 [ 54.95 - 74.92] | 88.09 [ 74.82 - 99.26] |
cbor | 334 [ +43%] | 16.38 [ 13.99 - 18.86] | 80.14 [ 68.09 - 96.12] | 98.78 [ 83.80 - 115.44] |
json | 402 [ +72%] | 29.18 [ 24.55 - 34.15] | 118.07 [ 93.40 - 126.61] | 156.74 [125.77 - 168.44] |
simd_json | 402 [ +72%] | 33.36 [ 26.10 - 37.58] | 126.90 [108.99 - 147.91] | 169.84 [144.93 - 197.78] |
flexbuffers | 453 [ +94%] | 134.07 [120.30 - 168.56] | 66.81 [ 60.73 - 84.08] | 215.41 [191.04 - 268.91] |
toml | 385 [ +65%] | 154.82 [129.26 - 181.64] | 317.42 [271.30 - 359.40] | 506.82 [442.59 - 574.69] |
2. MacOs Execution
Algorithm | Size (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] |
FlatMessage | 298 [ +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] |
rmp | 334 [ +43%] | 6.40 [ 6.38 - 6.47] | 27.50 [ 27.30 - 27.77] | 33.84 [ 33.66 - 34.55] |
bson | 376 [ +61%] | 17.01 [ 16.91 - 17.28] | 42.29 [ 42.12 - 42.55] | 59.32 [ 59.06 - 59.65] |
cbor | 334 [ +43%] | 13.65 [ 13.62 - 13.70] | 64.24 [ 64.04 - 64.67] | 78.41 [ 78.06 - 81.76] |
json | 402 [ +72%] | 29.79 [ 29.73 - 29.90] | 61.86 [ 61.52 - 62.38] | 91.77 [ 91.13 - 94.74] |
simd_json | 402 [ +72%] | 30.43 [ 30.33 - 31.44] | 90.45 [ 89.90 - 94.72] | 124.31 [122.48 - 128.54] |
flexbuffers | 453 [ +94%] | 84.15 [ 83.71 - 85.45] | 48.21 [ 47.58 - 48.85] | 134.98 [134.00 - 137.54] |
toml | 385 [ +65%] | 117.64 [115.60 - 120.67] | 199.51 [198.30 - 243.70] | 325.06 [323.88 - 332.56] |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 298 [ +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] |
rmp | 334 [ +43%] | 4.65 [ 4.51 - 6.99] | 27.44 [ 26.77 - 35.82] | 33.97 [ 32.88 - 36.01] |
bson | 376 [ +61%] | 16.35 [ 16.25 - 16.51] | 44.33 [ 43.72 - 47.85] | 65.05 [ 64.64 - 71.13] |
cbor | 334 [ +43%] | 14.22 [ 14.00 - 14.34] | 62.14 [ 61.48 - 62.66] | 79.70 [ 79.01 - 81.40] |
json | 402 [ +72%] | 29.82 [ 29.49 - 31.11] | 67.72 [ 66.33 - 72.12] | 102.07 [100.37 - 106.50] |
simd_json | 402 [ +72%] | 26.07 [ 25.57 - 27.85] | 75.24 [ 73.56 - 80.16] | 110.27 [108.32 - 113.10] |
flexbuffers | 453 [ +94%] | 101.88 [ 98.59 - 132.70] | 53.95 [ 53.63 - 83.60] | 172.32 [168.33 - 187.01] |
toml | 385 [ +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
Algorithm | Size (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] |
FlatMessage | 492 [ +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] |
rmp | 462 [ +11%] | 13.39 [ 12.67 - 13.72] | 114.01 [108.96 - 117.29] | 136.85 [129.60 - 140.75] |
json | 550 [ +33%] | 42.16 [ 40.85 - 44.91] | 183.08 [175.98 - 201.92] | 232.73 [225.25 - 252.79] |
cbor | 463 [ +12%] | 34.32 [ 32.90 - 35.54] | 214.70 [204.56 - 223.01] | 254.37 [246.49 - 262.38] |
simd_json | 550 [ +33%] | 37.78 [ 36.30 - 68.77] | 208.22 [197.99 - 212.99] | 260.11 [248.91 - 262.04] |
bson | 701 [ +69%] | 67.60 [ 64.28 - 70.99] | 207.89 [195.47 - 214.32] | 293.14 [284.69 - 301.15] |
flexbuffers | 561 [ +35%] | 236.30 [220.19 - 248.87] | 188.59 [182.44 - 194.91] | 442.59 [421.71 - 458.80] |
toml | 568 [ +37%] | 443.99 [428.89 - 507.61] | 790.28 [761.49 - 813.41] | 1284.78 [1249.50 - 1332.13] |
2. MacOs Execution
Algorithm | Size (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] |
FlatMessage | 492 [ +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] |
rmp | 462 [ +11%] | 15.60 [ 15.59 - 15.67] | 68.62 [ 68.49 - 69.66] | 83.70 [ 83.54 - 84.23] |
json | 550 [ +33%] | 39.29 [ 39.25 - 39.43] | 103.64 [101.90 - 111.56] | 143.19 [142.49 - 148.35] |
simd_json | 550 [ +33%] | 39.63 [ 39.55 - 40.35] | 125.81 [125.50 - 127.23] | 168.68 [167.92 - 169.57] |
cbor | 463 [ +12%] | 35.39 [ 35.25 - 35.55] | 153.95 [153.43 - 155.27] | 192.12 [191.47 - 209.82] |
bson | 701 [ +69%] | 58.97 [ 58.87 - 59.11] | 170.97 [168.88 - 173.12] | 237.09 [235.52 - 238.60] |
flexbuffers | 561 [ +35%] | 153.11 [152.18 - 154.83] | 112.54 [112.17 - 128.58] | 274.44 [273.39 - 275.55] |
toml | 568 [ +37%] | 320.21 [316.47 - 322.28] | 517.88 [514.60 - 519.24] | 857.90 [850.86 - 872.40] |
3. Linux Execution
Algorithm | Size (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] |
FlatMessage | 492 [ +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] |
rmp | 462 [ +11%] | 12.37 [ 11.73 - 14.78] | 63.32 [ 61.82 - 66.45] | 81.55 [ 78.86 - 94.16] |
json | 550 [ +33%] | 43.82 [ 42.84 - 46.60] | 103.38 [101.26 - 107.43] | 154.22 [151.84 - 187.59] |
simd_json | 550 [ +33%] | 39.29 [ 37.64 - 49.09] | 118.76 [114.23 - 126.63] | 170.38 [162.93 - 175.95] |
cbor | 463 [ +12%] | 34.88 [ 33.48 - 40.48] | 161.63 [157.12 - 179.40] | 198.24 [196.08 - 240.00] |
bson | 701 [ +69%] | 67.28 [ 65.60 - 75.67] | 144.59 [138.76 - 172.56] | 230.37 [220.26 - 268.93] |
flexbuffers | 561 [ +35%] | 173.97 [166.69 - 196.07] | 128.95 [127.13 - 196.57] | 319.99 [307.34 - 391.62] |
toml | 568 [ +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):
- 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, } }
- 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
Algorithm | Size (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] |
FlatMessagePacked | 185 [ +14%] | 19.02 [ 18.28 - 21.76] | 148.50 [137.33 - 153.57] | 169.07 [157.77 - 175.84] |
FlatMessageStruct | 217 [ +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
Algorithm | Size (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] |
FlatMessagePacked | 185 [ +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] |
FlatMessageStruct | 217 [ +34%] | 20.34 [ 20.07 - 20.40] | 95.30 [ 94.87 - 95.82] | 116.12 [113.39 - 116.94] |
3. Linux Execution
Algorithm | Size (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] |
FlatMessagePacked | 185 [ +14%] | 16.54 [ 15.89 - 26.07] | 68.38 [ 63.62 - 89.71] | 84.28 [ 80.95 - 89.62] |
FlatMessageStruct | 217 [ +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] |