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 work with different versions of your type, provided that the following rules are respected.
| 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 |
| The struct above | 14 bytes | 10 bytes |
| When to use | small size, best compatibility | smaller size, less compatibility |
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.
Format specifications
The complete version 1.0 wire formats are specified independently of the Rust API: