Two evolvable format variants
Postbag provides two wire formats with different trade-offs between schema flexibility and encoded size.
Postbag Full format with field names recommended
The Postbag Full format writes each field with its name and
the length of its value. That lets fields be added, removed and
reordered. Writing names as strings usually costs a lot of space but
Postbag reduces it to a single byte when #[serde(rename = "_n")]
is applied to a struct field or enum variant.
_0 to _59 take a
single byte, any other name is written out; keeping sensor as a
name would cost seven bytes rather than one. Enum variants can be numbered the
same way, and integers are varint-encoded, so a u32 of 300 just costs
two bytes.
Postbag Slim format without field names minimal size
The Postbag Slim format writes the values and nothing else, in the order
the fields are declared. Fields can still be added and removed at the end, but
nothing says which field is which, so backwards compatible changes to structs
and enums are limited.
Use Full by default. Choose Slim only when both
sides evolve together and minimum encoded size is more important than flexible
schema evolution.
Backwards and forwards compatibility
Serializer and deserializer can use different versions of a type when the changes follow these rules.
| Change to your types | Postbag Fullrecommended |
Postbag Slimminimal size |
|---|---|---|
| Structs | ||
| Add a field | anywhere | at the end |
| Remove a field | anywhere | at the end |
| Rename a field | when numbered | always |
| Reorder fields | yes | no |
| Enums | ||
| Add a variant | anywhere | at the end |
| Remove a variant | anywhere | at the end |
| Rename a variant | when numbered | always |
| Reorder variants | yes | no |
| Struct variant to newtype variant | yes | yes |
| Field types | ||
Wrap in Recoverable |
yes | no |
bool to Option<Recoverable<T>> |
yes | no |
| The struct above | 14 bytes | 10 bytes |
| When to use | small size, best compatibility | smaller size, less compatibility |
Why binary formats break when types change, and how Postbag avoids it →
Recover from incompatible values
Sometimes a type has to change incompatibly. With the Full format,
Postbag can confine a deserialization failure to a marked field, recover the
rest of the enclosing value, and replace that field with its default.
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize)]
struct Details {
size: u32,
}
#[derive(Deserialize, Serialize)]
struct Reading {
sensor: String,
#[serde(with = "postbag::recoverable")]
details: Details,
count: u16,
}
let bytes = postbag::to_full_vec(&reading)?;
let restored: Reading = postbag::from_full_slice(&bytes)?;
// If Details changed incompatibly, sensor and count are still
// restored, while details becomes Details::default().
Recoverable values can be added to an existing field without changing its wire representation.
The attribute is equivalent to wrapping the field type in
Recoverable<Details>,
which also allows custom recovery policies.
Format specifications
The complete version 1.0 wire formats are specified independently of the Rust API:
Common uses
How Postbag handles changing Rust types and files that outlive the release that wrote them.
-
Binary data that survives schema
changes
Why compact binary formats break after a type changes, and how Postbag keeps old and new programs compatible.
-
File formats that survive upgrades
How to keep configuration files, save files and documents compatible across application versions.