English | δΈζ
A high-performance Rust binary serialization/deserialization framework that automatically generates serialization code for custom types via derive macros.
- Zero-config derive macros β
#[derive(AFastSerialize, AFastDeserialize)]in one line - Rich type support β Primitives,
String,Vec<T>,Option<T>,[T; N],Box<T>, tuples,HashMap,HashSet,BTreeMap,BTreeSet, nested structs, enums - Generic support β Automatically adds trait bounds for generic parameters
- Configurable length prefix β Default
u32(max 4GB), switchable tou64via feature flag - Configurable enum tags β Default
u8, switchable tou16oru32via feature flags - Configurable tuple support β Default max 16 elements, switchable to 8 or 32 via feature flags
- Uniform little-endian β All multi-byte data uses little-endian encoding
- Zero runtime dependencies β No third-party dependencies at runtime
Add the dependency to your Cargo.toml:
[dependencies]
afastdata = "0.0.10"For u64 length prefix or custom enum tag type:
[dependencies]
afastdata = { version = "0.0.10", features = ["len-u64", "tag-u16"] }use afastdata::{AFastSerialize, AFastDeserialize};
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct User {
name: String,
age: u32,
email: Option<String>,
}
fn main() {
let user = User {
name: String::from("Alice"),
age: 30,
email: Some(String::from("alice@example.com")),
};
// Serialize
let bytes = user.to_bytes();
// Deserialize
let (decoded, consumed) = User::from_bytes(&bytes).unwrap();
assert_eq!(user, decoded);
println!("Consumed {} bytes", consumed);
}use afastdata::{AFastSerialize, AFastDeserialize};
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
enum Command {
Ping,
Send { to: String, message: String },
Broadcast(Vec<String>),
}
fn main() {
let cmd = Command::Send {
to: String::from("Bob"),
message: String::from("Hello!"),
};
let bytes = cmd.to_bytes();
let (decoded, _) = Command::from_bytes(&bytes).unwrap();
assert_eq!(cmd, decoded);
}use afastdata::{AFastSerialize, AFastDeserialize};
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct Response<T> {
code: u32,
data: Option<T>,
message: String,
}
fn main() {
let resp = Response {
code: 200,
data: Some(vec![1i32, 2, 3]),
message: String::from("ok"),
};
let bytes = resp.to_bytes();
let (decoded, _) = Response::<Vec<i32>>::from_bytes(&bytes).unwrap();
assert_eq!(resp, decoded);
}You can add validation rules to struct fields and enum variant fields using the #[afast(...)] attribute.
use afastdata::{AFastDeserialize, AFastSerialize, ValidateError};
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct A {
#[afast(
gt(10, 0, "${field} must be greater than 10"),
lte(100, 0, "${field} must be less than or equal to 100")
)]
a: i64,
#[afast(len(
10,
100,
1,
"${field} length must be between 10 and 100"
))]
b: Option<String>,
#[afast(func("v"))] // call v function to validate, parameter is `value` and `field name`
c: i32,
#[afast(skip("d"))] // call d function, #[afast(skip)] will call i64::default()
e: i64,
}
fn v(value: &i32, field: &str) -> Result<(), ValidateError> {
if *value % 2 == 0 {
Ok(())
} else {
Err(ValidateError::new(
2,
format!("{} must be an even number, but got {}", field, value),
))
}
}
fn d() -> i64 {
123
}Validation rules also work on named-field and tuple enum variants:
use afastdata::{AFastDeserialize, AFastSerialize, ErrorKind};
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
enum Command {
// Named-field variant
Login {
#[afast(len(1, 32, 1001, "username ${field} length must be 1-32"))]
username: String,
#[afast(len(6, 128, 1002, "password ${field} length must be 6-128"))]
password: String,
},
// Tuple variant
Send(#[afast(gte(0, 2001, "value must be >= 0"))] i64),
}
fn main() {
// Validation passes
let cmd = Command::Login {
username: String::from("alice"),
password: String::from("secret123"),
};
let bytes = cmd.to_bytes();
assert!(Command::from_bytes(&bytes).is_ok());
// Validation fails: empty username
let cmd = Command::Login {
username: String::new(),
password: String::from("secret123"),
};
let bytes = cmd.to_bytes();
let err = Command::from_bytes(&bytes).unwrap_err();
assert!(matches!(err.kind(), ErrorKind::ValidateError(1001, _)));
}Validation rules apply to both struct fields and enum variant fields (named and tuple variants):
skiporskip("default_fn")β Always skip this field during serialization and deserialization. On deserialize, fill with default value (call the given function, orDefault::default())skip_with("marker")orskip_with("marker", "default_fn")β Conditionally skip. When the marker passed toto_bytes_with/from_bytes_withmatches this field's marker, the field is skipped. Otherwise it is serialized/deserialized normally. The marker automatically propagates to nested types β if a struct field contains another struct withskip_withfields, those inner fields also respond to the same markergt(value, code, message): field value must be greater thanvalue(supports integer and float literals), otherwise returnValidateError. Applicable to numeric types andOption<numeric>(Nonepasses validation)gte(value, code, message): field value must be greater than or equal tovalue, otherwise returnValidateError. Applicable to numeric types andOption<numeric>lt(value, code, message): field value must be less thanvalue, otherwise returnValidateError. Applicable to numeric types andOption<numeric>lte(value, code, message): field value must be less than or equal tovalue, otherwise returnValidateError. Applicable to numeric types andOption<numeric>len(min, max, code, message): field length must be betweenminandmax, applicable toString,Vec<T>,[T; N], orOption<T>wrapping these types (Nonepasses validation). Pass-1forminormaxto leave that bound unrestricted, e.g.len(3, -1, ...)means min length 3 with no max limitof([v1, v2, ...], code, message): field value must be one of[v1, v2, ...], otherwise returnValidateErrorfunc(name)β Call external function for validation. Signature:fn(value: &T, field: &str) -> Result<(), ValidateError>
Type checking: Validation rules check field type compatibility at compile time. For example,
gt/gte/lt/lteonly work on numeric types orOption<numeric>, andlenonly works on string and collection types. Incompatible combinations produce a compile error.
skip_with enables conditional field skipping based on a marker string, useful when the same type needs different serialization strategies in different contexts:
use afastdata::{AFastSerialize, AFastDeserialize};
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct Packet {
header: u32,
#[afast(skip_with("cache"))]
payload: Vec<u8>,
#[afast(skip_with("cache", "default_checksum"))]
checksum: u64,
}
fn default_checksum() -> u64 { 0 }
fn main() {
let pkt = Packet {
header: 1,
payload: vec![1, 2, 3],
checksum: 0xABCD,
};
// Full serialization (all fields included)
let full = pkt.to_bytes();
// Conditional serialization (skip fields with marker="cache")
let cached = pkt.to_bytes_with("cache");
// `cached` only contains the header β shorter than `full`
// Conditional deserialization
let (decoded, _) = Packet::from_bytes_with(&cached, "cache").unwrap();
assert_eq!(decoded.header, 1);
assert_eq!(decoded.payload, Vec::new()); // Default::default()
assert_eq!(decoded.checksum, 0); // default_checksum()
}to_bytes_with(marker) and from_bytes_with(data, marker) accept a marker string:
- Marker matches the field's
skip_withtag β serialize skips the field; deserialize fills with default value or custom function - Marker does not match β behavior is identical to
to_bytes()/from_bytes() - Nested propagation β when serializing/deserializing a struct with
_withmethods, the marker automatically propagates to all nested types. If an inner struct also hasskip_withfields, they will respond to the same marker - For primitive types (
i32,String,Vec<T>, etc.), the_withmethods default to calling the regular methods
skip_with works automatically across nested structs β no special configuration needed:
use afastdata::{AFastSerialize, AFastDeserialize};
#[derive(AFastSerialize, AFastDeserialize, Debug, Default, PartialEq)]
struct Inner {
id: u32,
#[afast(skip_with("cache"))]
data: Vec<u8>,
name: String,
}
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct Outer {
header: u32,
inner: Inner,
footer: u32,
}
fn main() {
let outer = Outer {
header: 1,
inner: Inner {
id: 42,
data: vec![1, 2, 3, 4, 5],
name: String::from("test"),
},
footer: 99,
};
// Full serialization β includes inner.data
let full = outer.to_bytes();
// Conditional β inner.data is skipped via marker propagation
let cached = outer.to_bytes_with("cache");
assert!(cached.len() < full.len());
// Deserialize β inner.data is filled with Default::default()
let (decoded, _) = Outer::from_bytes_with(&cached, "cache").unwrap();
assert_eq!(decoded.inner.id, 42);
assert_eq!(decoded.inner.data, Vec::new());
assert_eq!(decoded.inner.name, String::from("test"));
}All internal variables in generated code are prefixed with __afast_ to avoid conflicts with your field names. You can safely use field names like data, offset, or bytes without any issues:
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct Record {
data: Vec<u8>, // No conflict with internal `data` parameter
offset: u64, // No conflict with internal `offset` variable
bytes: String, // No conflict with internal `bytes` variable
}| Type | Serialization | Size |
|---|---|---|
i8, u8 |
little-endian | 1 byte |
i16, u16 |
little-endian | 2 bytes |
i32, u32 |
little-endian | 4 bytes |
i64, u64 |
little-endian | 8 bytes |
usize |
u64 little-endian (cross-platform) | 8 bytes |
i128, u128 |
little-endian | 16 bytes |
f32 |
IEEE 754 little-endian | 4 bytes |
f64 |
IEEE 754 little-endian | 8 bytes |
bool |
0x00=false, 0x01=true |
1 byte |
String |
LenInt prefix + UTF-8 bytes | Variable |
&str |
LenInt prefix + UTF-8 bytes (serialize only) | Variable |
Vec<T> |
LenInt element count + element-wise encoding | Variable |
Option<T> |
1-byte tag + data (only when Some) | Variable |
[T; N] |
Element-wise encoding, no length prefix | Fixed |
(A, B, ...) |
Element-wise encoding, no length prefix | Fixed/Variable |
Box<T> |
Same as T |
Same as T |
HashMap<K, V> |
LenInt entry count + key-value pair encoding | Variable |
HashSet<T> |
LenInt element count + element-wise encoding | Variable |
BTreeMap<K, V> |
LenInt entry count + key-value pair encoding | Variable |
BTreeSet<T> |
LenInt element count + element-wise encoding | Variable |
| Struct | Field-by-field encoding, no extra prefix | Variable |
| Enum | Tag(u8/u16/u32) variant index + variant field data | Variable |
Enable via feature flags. All types below follow the same encoding conventions as built-in types.
| Type | Feature | Encoding | Size |
|---|---|---|---|
Ipv4Addr |
(std, always available) | 4-byte octets | 4 bytes |
Ipv6Addr |
(std, always available) | 16-byte octets | 16 bytes |
IpAddr |
(std, always available) | Tag (V4=0/V6=1) + address bytes | 5 or 17 bytes |
uuid::Uuid |
uuid |
16-byte UUID | 16 bytes |
chrono::NaiveDate |
chrono |
i32 (year) + u8 (month) + u8 (day) | 6 bytes |
chrono::NaiveTime |
chrono |
u64 (nanoseconds since midnight) | 8 bytes |
chrono::NaiveDateTime |
chrono |
NaiveDate + NaiveTime | 14 bytes |
chrono::DateTime<Utc> |
chrono |
i64 (timestamp) + u32 (subsec_nanos) | 12 bytes |
chrono::DateTime<Local> |
chrono |
i64 (timestamp) + u32 (subsec_nanos) | 12 bytes |
rust_decimal::Decimal |
rust_decimal |
i128 (mantissa) + u32 (scale) | 20 bytes |
url::Url |
url |
LenInt + UTF-8 bytes (same as String) | Variable |
bytes::Bytes |
bytes |
LenInt + raw bytes (same as Vec<u8>) |
Variable |
bytes::BytesMut |
bytes |
LenInt + raw bytes (same as Vec<u8>) |
Variable |
BigDecimal |
bigdecimal |
sign + LenInt + BigInt bytes + i64 exponent | Variable |
Ipv4Network |
ipnetwork |
4-byte address + 1-byte prefix length | 5 bytes |
Ipv6Network |
ipnetwork |
16-byte address + 1-byte prefix length | 17 bytes |
IpNetwork |
ipnetwork |
Tag (V4=0/V6=1) + network bytes | 6 or 18 bytes |
MacAddress |
mac_address |
6-byte MAC | 6 bytes |
sqlx::types::Json<T> |
sqlx |
Same as inner type T |
Same as T |
use afastdata::{AFastSerialize, AFastDeserialize};
// uuid
let id = uuid::Uuid::nil();
let bytes = id.to_bytes();
let (decoded, _) = <uuid::Uuid as AFastDeserialize>::from_bytes(&bytes).unwrap();
// chrono
let dt = chrono::DateTime::from_timestamp(1700000000, 0).unwrap();
let bytes = dt.to_bytes();
let (decoded, _) = <chrono::DateTime<chrono::Utc> as AFastDeserialize>::from_bytes(&bytes).unwrap();
// url
let url = url::Url::parse("https://example.com/path").unwrap();
let bytes = url.to_bytes();
let (decoded, _) = <url::Url as AFastDeserialize>::from_bytes(&bytes).unwrap();
// bytes
let b = bytes::Bytes::from(vec![1u8, 2, 3]);
let bytes = b.to_bytes();
let (decoded, _) = <bytes::Bytes as AFastDeserialize>::from_bytes(&bytes).unwrap();All fields are serialized in declaration order with no additional prefix:
[field1 bytes][field2 bytes][field3 bytes]...
Writes a variant index (Tag type, default u8, switchable to u16 or u32 via feature flags, starting from 0, incrementing by declaration order), followed by the variant's field data:
[Tag variant_index][field1 bytes][field2 bytes]...
Unit variants only write the index, with no field data.
Variable-length types like String and Vec<T> use LenInt as the length prefix:
- Default:
u32little-endian (4 bytes, max ~4GB) - With
len-u64feature:u64little-endian (8 bytes)
| Feature | Description | Default |
|---|---|---|
len-u64 |
Switch the length prefix from u32 to u64 |
No |
tag-u8 |
Enum variant tag uses u8 (1 byte, max 256 variants) |
Yes |
tag-u16 |
Enum variant tag uses u16 (2 bytes, max 65536 variants) |
No |
tag-u32 |
Enum variant tag uses u32 (4 bytes, max ~4.2 billion variants) |
No |
tuple-8 |
Tuple support up to 8 elements | No |
tuple-16 |
Tuple support up to 16 elements | Yes |
tuple-32 |
Tuple support up to 32 elements | No |
serde_json |
serde_json::Value / serde_json::Number support |
No |
uuid |
uuid::Uuid support |
No |
chrono |
NaiveDate / NaiveTime / NaiveDateTime / DateTime<Utc> / DateTime<Local> support |
No |
rust_decimal |
rust_decimal::Decimal support |
No |
url |
url::Url support |
No |
bytes |
bytes::Bytes / bytes::BytesMut support |
No |
bigdecimal |
bigdecimal::BigDecimal support |
No |
ipnetwork |
Ipv4Network / Ipv6Network / IpNetwork support |
No |
mac_address |
MacAddress support |
No |
sqlx |
sqlx::types::Json<T> wrapper support (implies serde_json) |
No |
afastdata/
βββ Cargo.toml # Workspace configuration
βββ README.md # English documentation (this file)
βββ README_CN.md # δΈζζζ‘£
βββ afastdata/ # Core library + unified entry crate
β βββ Cargo.toml # Contains `len-u64`, `tag-*`, `tuple-*` features
β βββ src/lib.rs # Trait definitions + primitive type implementations + re-exports derive macros
β βββ tests/
β βββ derive_tests.rs # Derive macro integration tests
β βββ primitive_tests.rs # Primitive type serialization tests
βββ afastdata-macro/ # Proc-macro library
βββ Cargo.toml
βββ src/lib.rs # AFastSerialize / AFastDeserialize derive macros
cargo run --example basic -p afastdatacargo test --workspaceMIT