Cord v2: one value, exactly one byte string
Cord is a serialization format for Rust with one property that drives everything else: the same value always encodes to the same bytes. On any platform, in any process, this year or next. That single guarantee is what lets you sign, hash, content-address, cache, and deduplicate serialized data.
use cord::{serialize, Cord};
#[derive(Cord)]
struct User { id: u32, name: String, active: bool }
let user = User { id: 42, name: "Alice".into(), active: true };
let bytes = serialize(&user).unwrap();
// Same value -> same bytes, every time, on every machine.
If you derive serde today, Cord is a drop-in encoder. #[derive(Cord)] only
adds the Cord-specific field attributes. Booleans, integers (i8–i128, u8–u128),
floats, strings, byte arrays, options, sequences, structs, and enums work out of
the box, plus DateTime, Map, Set, Decimal, and Uuid as first-class
types with no annotations.
What you can build with it
Sign a struct, verify it anywhere. Because a value has exactly one encoding,
you can serialize, sign the bytes, ship them, and re-serialize on the far side to
verify against identical bytes. No canonicalization dance, no "sign the JSON
string you happened to receive." Turn on the hash feature and
cord::hash() gives you a canonical SHA3-256 over any serializable value:
use cord::{hash, Cord};
#[derive(Cord)]
struct Receipt { order: u64, total_cents: u64, paid: bool }
let a = hash(&Receipt { order: 7, total_cents: 1999, paid: true }).unwrap();
let b = hash(&Receipt { order: 7, total_cents: 1999, paid: true }).unwrap();
assert_eq!(a, b); // content-addressing, dedup, and caching all fall out of this
Store data now, read it after the schema moves on. Wrap a field in
Evolving<T> and Cord length-prefixes its payload. When a newer service adds an
enum variant your code has never heard of, you read it as Evolving::Unknown,
and re-serializing hands back the exact bytes you received: no silent data loss,
no "unknown variant" panic in the middle of a pipeline.
use cord::{Cord, Evolving};
#[derive(Cord)]
enum Status { Active, Inactive } // a future version may add more
#[derive(Cord)]
struct Message { id: u32, status: Evolving<Status> }
// Old readers round-trip tomorrow's variants byte-for-byte.
Shrink the wire when it matters, and only then. The default is fixed-width
big-endian, which is predictable and fast to encode and decode. For a
size-sensitive protocol, drop #[cord(varint)] on a field for LEB128/zigzag, or
#[cord(width = 8)] to narrow a length prefix or variant index from the 32-bit
default down to one byte. You buy compactness per field, where you decide it's
worth the trade.
Why it holds
The determinism isn't a mode you switch on; it's a property of the format, so
you can't accidentally turn it off. Sets serialize in sorted order. Maps
serialize sorted by key. Strings are NFC-normalized on the way in, and the
deserializer rejects any string that isn't already NFC. Floats reject NaN and
canonicalize -0.0 to +0.0. The deserializer rejects trailing bytes instead of
quietly ignoring them, and caps nesting depth at 128 so hostile input can't blow
the stack.
That last set of choices is a threat model. Cord is built for the places where representation ambiguity becomes a vulnerability:
- Canonicalization bypass: inputs crafted to verify against a normalized form but execute against a different raw form (the shape of XML signature-wrapping and JWT header-manipulation attacks).
- Protocol confusion: data parsed one way by the subsystem that authorizes it and another way by the subsystem that acts on it.
- Reproducibility failures: third parties unable to independently reproduce the exact authenticated byte sequence, which breaks transparency logs and consensus.
A format where one value has exactly one legal encoding closes the gap those attacks live in.
What we deliberately left out
Cord is a general-purpose serialization format, but an opinionated one, and the opinions are the point. Determinism is mandatory, not a mode: the deserializer rejects non-canonical input (non-NFC strings, trailing bytes, non-minimal forms) rather than accepting it leniently, so Cord is a poor fit anywhere you need to round-trip flexible or third-party-lenient data as-is. Schema evolution is additive: you can add fields, but removing one breaks compatibility. The output is binary, so you'll want tooling to eyeball it, and the wire format changes between major versions, so think twice before using it as a decade-long at-rest format you can't re-encode.
Cord also does nothing for side-channels, memory-safety bugs outside its own code, resource exhaustion from oversized inputs, or weaknesses in the crypto primitives you feed its bytes into. It gives you canonical bytes; what you sign them with is your call, and your risk. If you need lenient parsing, free schema restructuring, or a text format a human can read raw, reach for protobuf or JSON and handle canonicalization yourself, carefully.
Try it
cargo add cord
Cord has seen production use in Backbone and is
Apache-2.0 licensed. Don't take the determinism claim on faith; read
src/ser.rs and check that a value has one encoding. Feature requests and bugs
go to https://github.com/backbone-hq/cord/issues; security reports follow
SECURITY.md.
Serialization is the layer where data gets defined. Backbone builds from the bottom up, because you can't secure what sits on a broken foundation, and a format where one value has two encodings is a broken foundation.
Upgrading from v1? The v2 wire format is a clean break: v1 bytes don't decode under v2. Migrate persisted data by deserializing with v1 and re-serializing with v2.