| Crates.io | senax-encoder |
| lib.rs | senax-encoder |
| version | 0.1.17 |
| created_at | 2025-05-24 06:36:44.59129+00 |
| updated_at | 2025-05-27 13:02:08.23892+00 |
| description | A fast, compact, and schema-evolution-friendly binary serialization library for Rust. |
| homepage | |
| repository | https://github.com/yossyX/senax-encoder |
| max_upload_size | |
| id | 1687059 |
| size | 313,592 |
A fast, compact, and schema-evolution-friendly binary serialization library for Rust.
You can control encoding/decoding behavior using the following attributes:
#[senax(id = N)] — Assigns a custom field or variant ID (u64). Ensures stable wire format across versions.#[senax(default)] — If a field is missing during decoding, its value is set to Default::default() instead of causing an error. For Option<T>, this means None.#[senax(skip_encode)] — This field is not written during encoding. On decode, it is set to Default::default().#[senax(skip_decode)] — This field is ignored during decoding and always set to Default::default(). It is still encoded if present.#[senax(skip_default)] — This field is not written during encoding if its value equals the default value. On decode, missing fields are set to Default::default().#[senax(rename = "name")] — Use the given string as the logical field/variant name for ID calculation. Useful for renaming fields/variants while keeping the same wire format.The following optional features enable support for popular crates and types:
chrono — Enables encoding/decoding of chrono::DateTime, NaiveDate, and NaiveTime types.uuid — Enables encoding/decoding of uuid::Uuid.ulid — Enables encoding/decoding of ulid::Ulid (shares the same tag as UUID for binary compatibility).rust_decimal — Enables encoding/decoding of rust_decimal::Decimal.indexmap — Enables encoding/decoding of IndexMap and IndexSet collections.fxhash — Enables encoding/decoding of fxhash::FxHashMap and fxhash::FxHashSet (fast hash collections).ahash — Enables encoding/decoding of ahash::AHashMap and ahash::AHashSet (high-performance hash collections).smol_str — Enables encoding/decoding of smol_str::SmolStr (small string optimization).serde_json — Enables encoding/decoding of serde_json::Value for dynamic JSON data.Add to your Cargo.toml:
[dependencies]
senax-encoder = "0.1"
Basic usage:
use senax_encoder::{Encode, Decode};
#[derive(Encode, Decode, Debug, PartialEq)]
struct User {
id: u32,
name: String,
email: Option<String>,
}
let user = User { id: 42, name: "Alice".into(), email: Some("alice@example.com".into()) };
// Schema evolution support (with field IDs)
let mut bytes = senax_encoder::encode(&user).unwrap();
let decoded: User = senax_encoder::decode(&mut bytes).unwrap();
assert_eq!(user, decoded);
// Compact encoding (without field IDs, smaller size)
let mut packed = senax_encoder::pack(&user).unwrap();
let unpacked: User = senax_encoder::unpack(&mut packed).unwrap();
assert_eq!(user, unpacked);
#[derive(Encode, Decode)]
struct MyStruct {
#[senax(id=1)]
foo: u32,
bar: Option<String>,
}
let mut bytes = senax_encoder::encode(&value)?;
let value2: MyStruct = senax_encoder::decode(&mut bytes)?;
// Pack for maximum compactness (no field IDs, smaller size)
let mut bytes = senax_encoder::pack(&value)?;
let value2: MyStruct = senax_encoder::unpack(&mut bytes)?;
// Note: pack/unpack is field-order dependent and doesn't support schema evolution
// Use when you need maximum performance and size optimization
#[senax(id=...)] only if you need to resolve a collision.Option become None if missing.default will cause a decode error if missing.indexmap, chrono, rust_decimal, uuid, ulid, serde_json, etc.u8~u128, i8~i128, f32, f64, bool, String, Bytes (zero-copy binary data)When respective features are enabled:
DateTime<Utc>, DateTime<Local>, NaiveDate, NaiveTimeUuidUlidDecimalIndexMap, IndexSetFxHashMap, FxHashSet (fast hash collections)AHashMap, AHashSet (high-performance hash collections)SmolStr (small string optimization)Value (dynamic JSON data)The senax-encoder supports automatic type conversion for compatible types during decoding, enabling schema evolution. However, certain conversions are not supported due to precision or data loss concerns.
u16 → u32)f64 can be decoded as f32 (with potential precision loss)T can be decoded as Option<T>Option<T> cannot be automatically decoded as T (use explicit handling)Example of compatible schema evolution:
// Version 1
#[derive(Encode, Decode)]
struct User {
id: u32, // Will be compatible with u64 in v2
name: String,
}
// Version 2 - Compatible changes
#[derive(Encode, Decode)]
struct User {
id: u64, // ✅ u32 → u64 automatic conversion
name: String,
email: Option<String>, // ✅ New optional field
#[senax(default)]
age: u32, // ✅ New field with default
}
When implementing custom Encoder and Decoder traits for your types, follow these important guidelines to ensure proper binary format consistency:
encode() call that writes all necessary data atomically.// ❌ WRONG: Multiple separate encode calls
impl Encoder for MyType {
fn encode(&self, writer: &mut BytesMut) -> Result<()> {
self.field1.encode(writer)?; // First encode call
self.field2.encode(writer)?; // Second encode call - WRONG!
Ok(())
}
}
// ✅ CORRECT: Single encode call with tuple
impl Encoder for MyType {
fn encode(&self, writer: &mut BytesMut) -> Result<()> {
(self.field1, self.field2).encode(writer) // Single encode call
}
}
use senax_encoder::{Encoder, Decoder, EncoderError};
use bytes::{BytesMut, Bytes};
struct Point3D {
x: f64,
y: f64,
z: f64,
}
impl Encoder for Point3D {
fn encode(&self, writer: &mut BytesMut) -> senax_encoder::Result<()> {
// ✅ Encode as single tuple
(self.x, self.y, self.z).encode(writer)
}
fn is_default(&self) -> bool {
self.x == 0.0 && self.y == 0.0 && self.z == 0.0
}
}
impl Decoder for Point3D {
fn decode(reader: &mut Bytes) -> senax_encoder::Result<Self> {
// ✅ Decode the same tuple structure
let (x, y, z) = <(f64, f64, f64)>::decode(reader)?;
Ok(Point3D { x, y, z })
}
}
struct CustomFormat {
header: String,
data: Vec<u8>,
checksum: u32,
}
impl Encoder for CustomFormat {
fn encode(&self, writer: &mut BytesMut) -> senax_encoder::Result<()> {
// ✅ Group all fields into a single tuple
(
&self.header,
&self.data,
self.checksum
).encode(writer)
}
fn is_default(&self) -> bool {
self.header.is_empty() && self.data.is_empty() && self.checksum == 0
}
}
impl Decoder for CustomFormat {
fn decode(reader: &mut Bytes) -> senax_encoder::Result<Self> {
// ✅ Decode the same tuple structure
let (header, data, checksum) = <(String, Vec<u8>, u32)>::decode(reader)?;
Ok(CustomFormat { header, data, checksum })
}
}
Format consistency: Each value gets exactly one tag in the binary format
Schema evolution: The library can properly skip unknown fields during forward/backward compatibility
Note: For most use cases, prefer using #[derive(Encode, Decode)] which automatically follows these best practices.