# zerocbor
A zero-copy, zero-dependency, no_std-compatible, extremely fast CBOR ([RFC 8949](https://www.rfc-editor.org/rfc/rfc8949)) serializer for Rust.
[](https://crates.io/crates/zerocbor)
[](https://docs.rs/zerocbor)
## Overview
zerocbor is a fast CBOR serializer for Rust. It runs about 1.5–4.0 times faster than other crates and is implemented without depending on any libraries, including `std`.
zerocbor is based on the architecture of [zerompk](https://github.com/nuskey8/zerompk), with the serialization format switched to CBOR. zerompk is a fast MessagePack serializer characterized by high performance and a small code size compared with conventional serializers. See the zerompk README for details.
## Quick Start
```rust
use zerocbor::{FromCbor, ToCbor};
#[derive(FromCbor, ToCbor)]
pub struct Person {
pub name: String,
pub age: u32,
}
fn main() {
let person = Person {
name: "Alice".to_string(),
age: 18,
};
let cbor: Vec<u8> = zerocbor::to_cbor_vec(&person)
.unwrap();
let person: Person = zerocbor::from_cbor(&cbor)
.unwrap();
}
```
## Format
The mapping between Rust types and CBOR types in zerocbor is as follows.
| `bool` | 7, simple value `true` / `false` |
| `u8`, `u16`, `u32`, `u64`, `usize` | 0, unsigned integer |
| `i8`, `i16`, `i32`, `i64`, `isize` | 0 or 1, unsigned or negative integer |
| `f32` | 7, `float 16` or `float 32`, whichever is narrower |
| `f64` | 7, whichever of the three float widths is narrowest |
| `char` | 3, one-character text string |
| `str`, `String` | 3, text string |
| `&'a [u8]` _(decode)_ | 2, byte string |
| `&[u8]`, `Vec<u8>` _(encode)_ | 4, array of integers — see the note below |
| `&[T]`, `Vec<T>`, `VecDeque<T>`, `LinkedList<T>`, `BTreeSet<T>`, `BinaryHeap<T>`, `HashSet<T>` | 4, array |
| `BTreeMap<K, V>`, `HashMap<K, V>` | 5, map |
| `()` | 7, simple value `null` |
| `Option<T>` | 7, `null` (`None`) or `T` (`Some(T)`) |
| `Result<T, E>` | 4, `[true, T]` (`Ok`) or `[false, E]` (`Err`) |
| newtype `struct W(T)` | as `T`, optionally under a tag |
| `(T0, T1)`, `(T0, T1, T2)`, ... | 4, array |
| `Box<T>`, `Rc<T>`, `Arc<T>` | as `T` |
| `PhantomData<T>` | 7, simple value `null` |
| struct (default, array representation) | 4, array of the fields in declaration order |
| struct (with `#[cbor(map)]`) | 5, map keyed by field name |
| enum, fieldless variants (default) | 0, the variant index |
| enum, fieldless variants (with `#[cbor(map)]`) | 3, the variant name |
| enum with data (default) | 4, `[index, fields...]` |
| enum with data (with `#[cbor(map)]`) | 5, `{"Name": value}` where value is the fields |
| anything with `#[cbor(tag = N)]` | 6, the tag, then the value it applies to |
## derive
Enable the `derive` feature flag to implement `FromCbor`/`ToCbor` using `derive` macros.
```rust
use zerocbor::{FromCbor, ToCbor};
#[derive(FromCbor, ToCbor)]
pub struct Person {
pub name: String,
pub age: u32,
}
```
You can also customize the serialization format using the `#[cbor]` attribute.
### array/map
You can choose `array` or `map` as the serialization format for structs and enums. For performance reasons, the default is `array`.
```rust
use zerocbor::{FromCbor, ToCbor};
#[derive(FromCbor, ToCbor)]
#[cbor(array)] // default
pub struct PersonArray {
pub name: String,
pub age: u32,
}
#[derive(FromCbor, ToCbor)]
#[cbor(map)]
pub struct PersonMap {
pub name: String,
pub age: u32,
}
```
### key
You can override the index/key used for fields and enum variants. Integers can be used for arrays, and strings for maps. When the format is `array` and there are gaps in the indices, `null` is inserted automatically.
```rust
#[derive(FromCbor, ToCbor)]
#[cbor(map)]
pub struct Point {
#[cbor(key = "the-x")]
x: i32,
y: i32,
}
```
> [!NOTE]
> To improve versioning resilience, it is recommended to set keys explicitly whenever possible.
### ignore
Set `ignore` on fields that should be ignored during serialization/deserialization. When deserializing a struct that contains an `ignore` field, the field's type must implement `Default`.
```rust
#[derive(FromCbor, ToCbor)]
pub struct Person {
pub name: String,
pub age: u32,
#[cbor(ignore)]
pub meta: Metadata,
}
```
### c_enum
Adding `#[cbor(c_enum)]` to a C-style enum allows it to be serialized as an integer. The value is the discriminant of each variant.
```rust
#[derive(FromCbor, ToCbor)]
#[cbor(c_enum)]
#[repr(u8)]
pub enum Status {
Ok = 0,
NotFound = 4,
InternalServerError = 5,
}
```
### as_bytes
You can specify whether a `u8` array is serialized as binary data (major type 2). The default is `true`. This option can be applied to fields of type `&[u8]`, `Vec<u8>`, or `Cow<[u8]>`.
```rust
use std::borrow::Cow;
use zerocbor::{FromCbor, ToCbor};
#[derive(Debug, PartialEq, ToCbor, FromCbor)]
struct Blob<'a> {
#[cbor(as_bytes = true)]
data: Cow<'a, [u8]>,
}
let value = Blob {
data: Cow::Borrowed(&[0x01, 0x02][..]),
};
let encoded = zerocbor::to_cbor_vec(&value).unwrap();
assert_eq!(encoded, vec![0x81, 0x42, 0x01, 0x02]);
assert_eq!(zerocbor::from_cbor::<Blob<'_>>(&encoded).unwrap(), value);
```
### default
If a key is missing during deserialization, the corresponding field is replaced with a default value. A missing key is filled with `Default::default()`, or, if `default = "path"` is specified, with the result of the named function.
This is supported only with `#[cbor(map)]`. (Because arrays have no field names, missing values cannot be detected safely.)
```rust
fn default_age() -> u32 {
18
}
use zerocbor::{FromCbor, ToCbor};
#[derive(Debug, PartialEq, ToCbor, FromCbor)]
#[cbor(map)]
pub struct Person {
pub name: String,
#[cbor(default)]
pub nickname: Option<String>,
#[cbor(default = "default_age")]
pub age: u32,
}
```
`default` applies only to missing keys. Unknown keys cause an error unless `allow_unknown_fields` is set.
### allow_unknown_fields
When an unknown key is encountered during deserialization, this changes the behavior to skip it instead of returning an error. This is effective only with `#[cbor(map)]`.
```rust
use zerocbor::{FromCbor, ToCbor};
#[derive(Debug, PartialEq, ToCbor, FromCbor)]
#[cbor(map, allow_unknown_fields)]
pub struct Person {
pub name: String,
pub age: u32,
}
```
To ensure full forward and backward compatibility, combine `default` and `allow_unknown_fields`.
```rust
#[derive(FromCbor, ToCbor)]
#[cbor(map, allow_unknown_fields)]
pub struct Person {
pub name: String,
#[cbor(default)]
pub age: u32,
}
```
> [!NOTE]
> These attributes are opt-in. By default, zerocbor requires an exact schema match.
## Benchmarks
> Measured on macOS 26.4.1 (arm64) with `rustc 1.100.0-nightly`.
### Serialize/Deserialize Struct (2 fields, array format)
| `cbor4ii` | 3.19 μs | 16.46 μs |
| `ciborium` | 18.40 μs | 123.92 μs |
| `minicbor` | 11.36 μs | 10.29 μs |
| **`zerocbor`** | 1.54 μs | 5.03 μs |
### Serialize/Deserialize Struct (4 fields, map format, with a nested struct, an `Option` and a `Vec`)
| `cbor4ii` | 34.52 μs | 180.49 μs |
| `ciborium` | 90.69 μs | 491.88 μs |
| `minicbor` | 64.42 μs | 150.32 μs |
| **`zerocbor`** | 24.74 μs | 122.83 μs |
### Serialize/Deserialize Struct (8 integer fields, one of every width)
| `ciborium` | 87.40 μs | 357.90 μs |
| `minicbor` | 58.71 μs | 41.40 μs |
| **`zerocbor`** | 8.54 μs | 27.15 μs |
### Serialize/Deserialize Array (a 2-field struct plus a 1000-element `Vec<u64>`)
| `cbor4ii` | 2,289.96 μs | 6,544.41 μs |
| `ciborium` | 6,175.27 μs | 15,511.02 μs |
| `minicbor` | 11,050.64 μs | 5,625.04 μs |
| **`zerocbor`** | 820.06 μs | 2,376.10 μs |
### Serialize/Deserialize Struct (borrowed `&str` and byte string)
| `cbor4ii` | 11.74 μs | 27.33 μs |
| `ciborium` | 23.84 μs | N/A |
| `minicbor` | N/A | N/A |
| **`zerocbor`** | 8.71 μs | 17.80 μs |
### Deserialize Array (1000 records) from a `std::io::Read` stream
| `cbor4ii` | 124.05 μs |
| `ciborium` | 96.32 μs |
| **`zerocbor`** | 32.62 μs |
### Decode/Encode a dynamically typed document into and out of `Value`
| `ciborium` | 2.83 μs | 496.22 μs |
| **`zerocbor`** | 1.32 μs | 365.69 μs |
## License
This library is released under the [MIT License](LICENSE).