Expand description
This crate provides a full-featured implementation of RFC 8949 — the Concise Binary Object Representation (CBOR) — built on serde.
CBOR adopts and modestly builds on the data model used by JSON, except the encoding is in binary form. Its primary goals include a balance of implementation size, message size and extensibility.
Feature coverage includes ordinary serde encode/decode, RFC 8949 preferred
serialization, deterministic/canonical encoders, a dynamic Value type,
validated RawValue pass-through bytes, CBOR simple values, semantic tags,
bignums, COSE-style integer map keys, arrays and tags through #[derive(Cbor)], CBOR
sequences, async item I/O through async_io, diagnostic notation,
exact-one-item validate, allocation-free serialization helpers and
no_std modes.
§Quick start
Use to_vec/to_writer to encode any serde::Serialize type and
from_slice/from_reader to decode any serde::Deserialize type.
from_slice can also borrow definite-length text and byte strings directly
from the input buffer:
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Photo {
title: String,
pixels: (u32, u32),
tags: Vec<String>,
}
let photo = Photo {
title: "Sunrise".into(),
pixels: (1920, 1080),
tags: vec!["morning".into(), "gradient".into()],
};
let bytes = cbor2::to_vec(&photo).unwrap();
let back: Photo = cbor2::from_slice(&bytes).unwrap();
assert_eq!(photo, back);from_slice and from_reader deserialize one leading CBOR item. Use
validate first when a byte buffer must contain exactly one item, or use
de::Deserializer::into_iter for a CBOR sequence.
§For code agents
Code agents should read the repository’s
AGENTS.md first for
the compressed API-selection contract, then use the
agent cookbook
and the agent_patterns example for copyable recipes.
§Command line tool
The workspace also publishes cbor2-cli, which installs the cbor
command for converting CBOR to and from JSON and for rendering diagnostic
notation:
brew install ldclabs/tap/cbor2-cli # Homebrew, installs cbor
cargo install cbor2-cli # Cargo, installs cbor§Byte strings and serde_bytes
Serde’s default data model treats Vec<u8> and &[u8] as sequences, so
they serialize as CBOR arrays, not byte strings. Use
serde_bytes when the
wire type should be major type 2.
let bytes = vec![1u8, 2, 3, 4];
// Bare Vec<u8>: [1, 2, 3, 4]
assert_eq!(hex::encode(cbor2::to_vec(&bytes).unwrap()), "8401020304");
// serde_bytes::ByteBuf: h'01020304'
let bytes = serde_bytes::ByteBuf::from(bytes);
assert_eq!(hex::encode(cbor2::to_vec(&bytes).unwrap()), "4401020304");For struct fields, use serde’s field adapter:
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Packet {
#[serde(with = "serde_bytes")]
payload: Vec<u8>,
}
let packet = Packet { payload: vec![0xde, 0xad, 0xbe, 0xef] };
assert_eq!(
hex::encode(cbor2::to_vec(&packet).unwrap()),
"a1677061796c6f616444deadbeef"
);When building dynamic data directly, Value::Bytes already represents a
CBOR byte string:
let value = cbor2::Value::Bytes(vec![0xde, 0xad]);
assert_eq!(hex::encode(cbor2::to_vec(&value).unwrap()), "42dead");§Borrowed Slice Deserialization
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize)]
struct Borrowed<'a> {
#[serde(borrow)]
label: &'a str,
#[serde(borrow, with = "serde_bytes")]
payload: &'a [u8],
}
let bytes = hex::decode("a2656c6162656c626869677061796c6f616442dead").unwrap();
let value: Borrowed<'_> = cbor2::from_slice(&bytes).unwrap();
assert_eq!(value, Borrowed { label: "hi", payload: &[0xde, 0xad] });Indefinite-length strings are accepted for owned targets, but cannot be borrowed because their logical body is split across segments.
§Dynamic values
When the shape of the data is not known in advance, decode into a
Value, the CBOR equivalent of serde_json::Value. The cbor! macro
builds Values with a JSON-like syntax:
use cbor2::{cbor, Simple, Value};
let value = cbor!({
"code": 415,
"message": null,
"tags": ["legacy", 1.5],
(Simple::new(59).unwrap()) => [Value::Bytes(vec![0xde, 0xad])],
}).unwrap();
let bytes = cbor2::to_vec(&value).unwrap();
let back: Value = cbor2::from_slice(&bytes).unwrap();
assert_eq!(value, back);
let simple: Simple = cbor2::from_slice(&[0xf8, 0x3b]).unwrap();
assert_eq!(simple, Simple::new(59).unwrap());Value::serialized and Value::deserialized convert between Value and
any type implementing the serde traits.
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Point {
x: i64,
y: i64,
}
let value = cbor2::Value::serialized(&Point { x: -2, y: 5 }).unwrap();
assert_eq!(value.to_string(), r#"{"x": -2, "y": 5}"#);
let point: Point = value.deserialized().unwrap();
assert_eq!(point, Point { x: -2, y: 5 });§Raw values
A RawValue keeps one CBOR item as its raw encoded bytes — validated
for well-formedness, but never decoded. Serializing splices the bytes
into the stream untouched and deserializing captures them byte for byte,
which preserves the exact wire encoding for signature payloads,
pass-through items and deferred decoding:
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Signed {
#[serde(with = "serde_bytes")]
signature: Vec<u8>,
payload: cbor2::RawValue,
}
let bytes = cbor2::to_vec(&Signed {
signature: vec![0xde, 0xad],
payload: cbor2::RawValue::serialized(&("untouched", 42)).unwrap(),
}).unwrap();
let signed: Signed = cbor2::from_slice(&bytes).unwrap();
// Verify `signed.signature` over `signed.payload.as_bytes()`, then:
let (text, n): (String, u8) = signed.payload.deserialized().unwrap();
assert_eq!((text.as_str(), n), ("untouched", 42));TryFrom converts in both directions between RawValue and Value:
decoding one way, encoding the other.
§CBOR sequences
CBOR sequences (RFC 8742) are streams of adjacent complete CBOR items.
Write them by calling to_writer repeatedly, and read them with
de::Deserializer::into_iter:
let mut stream = Vec::new();
cbor2::to_writer(&"hello", &mut stream).unwrap();
cbor2::to_writer(&42u64, &mut stream).unwrap();
let items: Vec<cbor2::Value> = cbor2::de::Deserializer::from_reader(&stream[..])
.into_iter()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(items, vec![cbor2::Value::from("hello"), cbor2::Value::from(42)]);
assert!(cbor2::validate(&stream[..]).is_err()); // not exactly one item§Async Item I/O
Serde itself is synchronous. The async_io module handles the async
transport boundary by reading or writing one complete CBOR item; once an item
is buffered, use the regular serde API. For untrusted peers, use
read_item_with_limit or read_value_with_limit unless an outer transport
layer already enforces a message size limit:
let value: cbor2::Value = cbor2::async_io::read_value_with_limit(reader, 1 << 20).await?;Enable the futures or tokio feature to call the matching
async_io::futures::* or async_io::tokio::* helpers directly with
futures_io::AsyncRead/AsyncWrite or tokio::io::AsyncRead/AsyncWrite
types.
§Tags
CBOR data items can be wrapped in semantic tags (RFC 8949 §3.4). The
wrapper types in the tag module capture and emit tags through serde:
use cbor2::tag::RequireExact;
// Tag 32: a URI.
type Uri = RequireExact<String, 32>;
let uri: Uri = RequireExact("https://example.com".into());
let bytes = cbor2::to_vec(&uri).unwrap();
assert_eq!(bytes[0], 0xd8); // tag(32)§Integer Map Keys, Arrays and Tags (COSE)
Protocols like COSE (RFC 9052) key their maps with integers, encode several
messages as arrays and wrap messages in tags, which serde’s data model cannot
express on named structs. With the derive feature, #[derive(Cbor)]
declares those details — a textual
#[serde(rename = "1")] stays a text key, so there is no ambiguity
between the two. The derive generates the Serialize and Deserialize
impls itself, so serde’s derives must not be repeated alongside it:
use cbor2::Cbor;
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 98)]
struct CoseSign {
#[cbor(key = 1)]
kty: u8,
#[cbor(key = 3)]
alg: i8,
}
let key = CoseSign { kty: 2, alg: -7 };
let bytes = cbor2::to_vec(&key).unwrap();
assert_eq!(hex::encode(&bytes), "d862a201020326"); // 98({1: 2, 3: -7})
assert_eq!(cbor2::from_slice::<CoseSign>(&bytes).unwrap(), key);The tag is transparent on decode, and the serde attributes (alias,
default, skip, with, …) work as usual; map types like
HashMap<String, _> are unaffected. The declared keys, array shape and tag
stay inspectable at runtime through the Cbor trait, which the
derive implements alongside the serde traits.
The derive touches neither the field names nor the type name — the
protocol details ride along on a hidden shadow type (see
ser::STRUCT_MARKER) recognized only by this crate’s serializers — so
the same type still serializes naturally everywhere else. JSON, for
example, just works, with the original field names and no tag:
let json = serde_json::to_string(&key).unwrap();
assert_eq!(json, r#"{"kty":2,"alg":-7}"#);
assert_eq!(serde_json::from_str::<CoseSign>(&json).unwrap(), key);For named Rust structs whose CBOR wire shape is an array, add
#[cbor(array)]:
use cbor2::Cbor;
#[derive(Debug, PartialEq, Cbor)]
#[cbor(tag = 18, array)]
struct Sign1 {
#[serde(with = "serde_bytes")]
protected: Vec<u8>,
unprotected: u8,
#[serde(with = "serde_bytes")]
payload: Vec<u8>,
#[serde(with = "serde_bytes")]
signature: Vec<u8>,
}
let msg = Sign1 {
protected: vec![0xa0],
unprotected: 0,
payload: vec![],
signature: vec![0xff],
};
assert_eq!(hex::encode(cbor2::to_vec(&msg).unwrap()), "d28441a0004041ff");
assert!(Sign1::ARRAY);§Allocation-free helpers
Three helpers work without touching the heap: validate checks that an
input is exactly one well-formed CBOR item (including text UTF-8 validity),
serialized_size computes the exact encoded size of any serializable
value, and to_slice encodes into a caller-provided buffer.
let value = ("hello", vec![1u8, 2, 3]);
let bytes = cbor2::to_vec(&value).unwrap();
assert_eq!(cbor2::serialized_size(&value).unwrap(), bytes.len() as u64);
assert!(cbor2::validate(&bytes[..]).is_ok());
assert!(cbor2::validate(&bytes[..bytes.len() - 1]).is_err()); // truncated
let mut buffer = [0u8; 16];
assert_eq!(cbor2::to_slice(&value, &mut buffer).unwrap(), &bytes[..]);§Crate features
std(default) — implements theiotraits for everystd::io::Read/std::io::Write, addsasync_ioand adds theHashMapconversions. Impliesalloc.alloc— everything that needs a heap, withoutstd:Value,to_vec/from_slice/from_reader,RawValue,diagnostic/diagnostic_pretty, the deterministic encoders and thecbor!macro. Readers and writers are byte slices,Vec<u8>, or customiotrait implementations.cdn— enables CDN input extensions that need external crates:hashpluscri/CRIliterals. Impliesalloc.- neither — a
#![no_std]core for constrained targets: streaming serialization withto_writer/to_slice/serialized_size,validate, thetagwrappers and thecoreheader codec. Deserializing through serde requiresalloc. derive— the#[derive(Cbor)]macro; works in all three modes (deserialization again requiringalloc).futures— addsasync_io::futuresadapters forfutures_io::AsyncRead/futures_io::AsyncWrite. Impliesstd.tokio— addsasync_io::tokioadapters fortokio::io::AsyncRead/tokio::io::AsyncWrite. Impliesstd.
§Concise Diagnostic Notation
The API names keep the two directions separate:
to_cdnandto_cdn_prettyrender CBOR bytes to CDN text.cdn_to_vec,cdn_sequence_to_vecandfrom_cdnparse CDN text to CBOR bytes or serde values.- The historical
diagnostic*names remain available as compatibility aliases for the correspondingto_cdn*functions.
to_cdn renders raw CBOR as the compact human-readable text form
formalized by the IETF Concise Diagnostic Notation draft
(draft-ietf-cbor-edn-literals); to_cdn_pretty does the same with
two-space indentation. Both work on the wire and preserve what a Value
cannot represent: indefinite-length markers, undefined, and unassigned
simple values. cdn_to_vec parses CDN text into CBOR bytes, including
comments, byte-string literals, embedded CBOR sequences, encoding indicators,
tags, simple values and the CDN application extensions implemented by this
crate. Enable the cdn feature for hash, cri, and CRI. Value
implements
Display with the same compact notation, and
Debug pretty-prints it with indentation.
When integer-keyed maps also have a known string-key table such as
Cbor::KEYS, to_cdn_pretty_with_key_comments can annotate the
pretty output with // "field" comments without changing the decoded
wire item.
let bytes = hex::decode("bf61610161629f0203ffff").unwrap();
assert_eq!(
cbor2::to_cdn(&bytes[..]).unwrap(),
r#"{_ "a": 1, "b": [_ 2, 3]}"#
);
assert_eq!(
cbor2::to_cdn_pretty(&bytes[..]).unwrap(),
"{_\n \"a\": 1,\n \"b\": [_\n 2,\n 3\n ]\n}"
);
let keyed = hex::decode("a201626d6504182a").unwrap();
let keys = [("iss", 1), ("exp", 4)];
assert_eq!(
cbor2::to_cdn_pretty_with_key_comments(&keyed[..], &keys).unwrap(),
"{\n 1: \"me\", // \"iss\"\n 4: 42 // \"exp\"\n}"
);
let value = cbor2::cbor!({ "k": [1, -2.5, null] }).unwrap();
assert_eq!(value.to_string(), r#"{"k": [1, -2.5, null]}"#);
let cdn = r#"{ /kty/ 1: 4, /k/ -1: h'6684523a' }"#;
let bytes = cbor2::cdn_to_vec(cdn).unwrap();
assert_eq!(cbor2::to_cdn(&bytes[..]).unwrap(), "{1: 4, -1: h'6684523a'}");§Low-level headers
The core module exposes the pull/push header codec for applications
that need to preserve wire structure such as indefinite-length strings:
use cbor2::core::{Decoder, Encoder, Header};
let mut bytes = Vec::new();
let mut enc = Encoder::from(&mut bytes);
enc.push(Header::Array(None)).unwrap();
enc.text("chunked").unwrap();
enc.bytes(&[0xde, 0xad]).unwrap();
enc.push(Header::Break).unwrap();
let mut dec = Decoder::from(&bytes[..]);
assert_eq!(dec.pull().unwrap(), Header::Array(None));
let Header::Text(len) = dec.pull().unwrap() else { unreachable!() };
let mut text = String::new();
dec.text_body(len, &mut text).unwrap();
assert_eq!(text, "chunked");
let Header::Bytes(len) = dec.pull().unwrap() else { unreachable!() };
let mut body = Vec::new();
dec.bytes_body(len, &mut body).unwrap();
assert_eq!(body, vec![0xde, 0xad]);
assert_eq!(dec.pull().unwrap(), Header::Break);§Deterministic encoding
to_canonical_vec/to_canonical_writer produce output satisfying the
core deterministic encoding requirements of RFC 8949 §4.2.1: preferred
(smallest) serializations, definite lengths only, and map keys sorted in the
bytewise lexicographic order of their encodings. Value::canonicalize
applies the same normalization to a Value in place.
use std::collections::HashMap;
// HashMap iteration order is random, but the encoding is stable.
let map: HashMap<&str, i32> = [("z", 1), ("aa", 2), ("b", 3)].into();
let bytes = cbor2::to_canonical_vec(&map).unwrap();
assert_eq!(bytes, cbor2::to_canonical_vec(&map).unwrap());
assert_eq!(hex::encode(&bytes), "a3616203617a01626161 02".replace(' ', ""));Many existing protocols instead use the older “Canonical CBOR” key order of
RFC 7049 §3.9 (kept as RFC 8949 §4.2.3), where shorter encoded keys sort
first. Pass KeyOrder::LengthFirst to the *_with variants for that:
use cbor2::KeyOrder;
let map: std::collections::HashMap<i64, bool> = [(100, true), (-1, false)].into();
// Bytewise (RFC 8949 §4.2.1): 100 (0x1864) sorts before -1 (0x20).
let core = cbor2::to_canonical_vec(&map).unwrap();
assert_eq!(hex::encode(&core), "a2 1864f5 20f4".replace(' ', ""));
// Length-first (RFC 7049 §3.9): -1 sorts before 100.
let legacy = cbor2::to_canonical_vec_with(&map, KeyOrder::LengthFirst).unwrap();
assert_eq!(hex::encode(&legacy), "a2 20f4 1864f5".replace(' ', ""));§Design decisions
This implementation is wire-compatible with
ciborium, whose design it follows:
- Numbers are always encoded in their smallest lossless form, as
deterministic encoding (RFC 8949 §4.2.1) requires. Integer width in Rust
is treated as an in-memory detail, not a wire property:
1u64encodes as one byte, and that byte happily decodes into au128or ani8. u128/i128values outside the 64-bit range are encoded as bignums (tags 2 and 3), and bignums small enough to fit are accepted for any integer type.- Maps are represented as
Vec<(Value, Value)>inValue, preserving wire order and arbitrary (even duplicate) keys. - Be liberal in what you accept: decoding handles indefinite-length items, segmented strings, half-width floats, leading zeros in bignums and unknown tags in most positions, even though encoding never produces most of those forms.
- Deeply nested input fails with
RecursionLimitExceededinstead of exhausting the stack; seede::Deserializer::with_recursion_limit.
§History
This crate descends from cbor by Andrew Gallant, whose 0.4 and earlier
releases were built on the long-deprecated rustc-serialize framework and
predate both serde 1.0 and RFC 8949. Version 0.5 was a from-scratch rewrite
published under the cbor2 name — the original crates.io name stays with
the legacy release — and 1.0 stabilizes it; none of the old API survives.
Re-exports§
pub use crate::cdn::cdn_sequence_to_vec;pub use crate::cdn::cdn_to_vec;pub use crate::cdn::from_cdn;pub use crate::cdn::to_cdn;pub use crate::cdn::to_cdn_pretty;pub use crate::cdn::to_cdn_pretty_with_key_comments;pub use crate::simple::Simple;
Modules§
- async_
io - Async helpers for complete CBOR item I/O.
- cdn
- Concise Diagnostic Notation (CDN) input helpers and output re-exports.
- core
- Low-level CBOR encoding and decoding.
- de
- Serde deserialization support for CBOR.
- io
- The reader and writer traits used by the encoder and decoder.
- ser
- Serde serialization support for CBOR.
- simple
- Helper type for preserving CBOR simple values (RFC 8949 §3.3).
- tag
- Helper types for capturing and emitting CBOR tags (RFC 8949 §3.4).
- value
- A dynamic CBOR value.
Macros§
Structs§
- RawValue
- A valid CBOR item, kept as its raw encoded bytes.
Enums§
- KeyOrder
- The map key ordering used by deterministic encoding.
- Value
- A representation of any CBOR item that can be inspected and manipulated dynamically.
Traits§
- Cbor
- The CBOR protocol details a
#[derive(Cbor)]type declares: its integer map keys, array shape and tag.
Functions§
- diagnostic
- Renders one CBOR item from a reader in diagnostic notation (RFC 8949 §8).
- diagnostic_
pretty - Like
diagnostic, but pretty-prints arrays and maps with two-space indentation, one element per line. - diagnostic_
pretty_ with_ key_ comments - Like
diagnostic_pretty, but annotates integer map keys with matching string keys as// "key"comments. - from_
reader - Deserializes a value from CBOR read out of a
Read. - from_
slice - Deserializes a value from a byte slice of CBOR.
- serialized_
size - Computes the exact number of bytes that
to_writerwould produce for a value, without writing or buffering anything. - to_
canonical_ vec - Serializes a value as deterministically encoded CBOR into a new
Vec<u8>, satisfying the core deterministic encoding requirements of RFC 8949 §4.2.1. - to_
canonical_ vec_ with - Serializes a value as deterministically encoded CBOR into a new
Vec<u8>, sorting map keys in the givenKeyOrder. - to_
canonical_ writer - Serializes a value as deterministically encoded CBOR into a
Write, satisfying the core deterministic encoding requirements of RFC 8949 §4.2.1. - to_
canonical_ writer_ with - Serializes a value as deterministically encoded CBOR into a
Write, sorting map keys in the givenKeyOrder. - to_
slice - Serializes a value as CBOR into the front of
buffer, returning the written prefix. - to_vec
- Serializes a value as CBOR into a new
Vec<u8>. - to_
writer - Serializes a value as CBOR into a
Write. - validate
- Checks that the input contains exactly one well-formed CBOR item.
Derive Macros§
- Cbor
- Derives
serde::Serializeandserde::Deserializewith CBOR protocol details: integer map keys, field-order arrays and a CBOR tag (COSE, RFC 9052).