Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Format Extensions

Advanced. You need this chapter to write a new format, and to understand why XML and protobuf annotations look different from ordinary #[wire(...)] ones. Using a shipped format needs only the short worked examples at the end.

Every attribute so far has been format-neutral: rename and flatten and default mean something in any format. But real formats need vocabulary the core cannot anticipate. XML has to distinguish an element from an attribute from text. Protobuf addresses fields by number and cares about proto2-vs-proto3 presence rules. A URL form has no single convention for repeated keys.

Serde’s answer to this is that there isn’t one: #[serde(...)] is a fixed set, so format-specific needs get solved by parallel derive ecosystems. The wire model’s answer is an extension bag in the binding that any format can write into and read back.

The mechanism: the ext bag

Every Binding, FieldBinding, and VariantBinding carries an ext slice of (&'static str, ExtVal) pairs:

pub enum ExtVal {
    Str(&'static str),
    Int(i64),
    Bool(bool),
}

Three value kinds, all 'static, all Copy — enough to express a field number, an element name, or a boolean mode, and nothing more. The keys are plain strings, and namespaced keys like "xml.root" keep formats from colliding.

You can write into the bag directly:

#[wire(ext("xml.root" = "Order", "proto.id" = 3))]

but you normally shouldn’t. The raw keys are an internal contract between an extension’s macro (which writes them) and its codec (which reads them) — which is why every shipped extension provides a decoration macro to write them and typed readers to read them, so neither library authors nor codec writers ever touch a stringly key.

Anatomy of a format contract

A format contract crate is three things, and the shipped ones are all built the same way:

  1. A marker. One zero-sized type — Xml, Protobuf, UrlForm — so a codec speaking the contract returns Codec<Xml> and an API boundary requiring XML names it. This is the whole of the JSON contract, since JSON is self-describing and needs no annotations at all.
  2. A decoration macro, behind a model feature. #[xml_extension], #[proto_extension], #[urlform_extension] — each accepts format-shaped attributes and lowers them into the core ext bag. Crucially, the macro expands to the core #[derive(WireModel, WireEncode, WireDecode)], so a decorated type is an ordinary wire-model type that happens to carry extra knobs.
  3. Typed readers. Functions like place(), root_name(), xmlns(), field_number(), list_convention() that read the bag back and return a real type instead of an Option<&str> a codec would have to interpret.

The model feature is off by default, and the reason is worth noting: a codec needs only the marker and the readers, never the macro. Gating the macro means a codec crate never compiles a proc-macro it will not use.

The division is the same one the contract/engine split draws everywhere else: the contract crate is tiny and codec-agnostic, and one or more byte engines implement it. capiw_ext_xml defines what XML annotations mean; capiw_quick_xml is one engine that honors them.

The shipped extensions

Four contract crates ship: capiw_ext_json (the Json marker and nothing else — JSON is self-describing), capiw_ext_xml, capiw_ext_urlencoded, and capiw_ext_protobuf. The three with vocabulary each carry a decoration macro behind an off-default model feature; a codec consumes the contract without it.

XML

XML needs to know where a field goes in the document. The decoration macro expands to the three wire derives itself, so a decorated type derives only its ordinary Rust traits:

#[xml_extension(root = "Grantee", xmlns = "http://s3.amazonaws.com/doc/2006-03-01/")]
#[derive(Debug, Clone, PartialEq)]
pub struct Grantee {
    #[xml(attribute)]
    #[wire(rename = "xsi:type")]
    pub kind: String,      // rendered as an attribute, not a child element
    pub id: String,        // a child element by default
}

The field annotations are #[xml(attribute)], #[xml(text)], and #[xml(element)], read back through place() (yielding an XmlPlace). The type-level root and xmlns arguments carry the root element name and namespace, read through root_name() and xmlns(). #[xml_extension] takes a named-field struct; an enum uses the plain derives.

URL forms

A form has no agreed encoding for a list field — APIs variously want repeated keys, bracketed keys, or one delimited value — and the choice is per field, so it rides on the field’s bag:

#[urlform_extension]
pub struct Filter {
    pub tag: Vec<String>,                    // bare, or #[urlform(repeated)]: tag=a&tag=b
    #[urlform(brackets)] pub id: Vec<u32>,   // id[]=1&id[]=2
    #[urlform(comma)] pub kind: Vec<Kind>,   // kind=a,b
}

#[urlform(delim = "…")] covers other separators, and the codec reads the choice back through list_convention(). The UrlForm marker also implements QueryFormat — the format emits key=value text — which is what makes Codec<UrlForm>::encode_qs exist for an endpoint’s query string. Note that the delimited forms lower to the codec-agnostic #[wire(joined = ...)] scalar rule rather than to an XML-style ext key — an extension should reach for a core rule when one already says what it means.

Protobuf

Protobuf is the case that justifies the whole design, because it is non-self-describing: fields are addressed by number, and the decoder needs the mapping before it sees a value.

#[proto_extension]
pub struct Order {
    #[proto(id = 1)] pub id: u64,
    #[proto(id = 2, sint)] pub delta: i32,
    #[proto(id = 3, packed)] pub tags: Vec<u32>,
    pub note: String,                        // no id: numbered sequentially, here 4
}

Fields without an id are numbered sequentially from the last explicit number, starting at 1 — most messages carry no #[proto(id)] at all and let declaration order number them. The other field knobs are sint / fixed (the integer wire encoding; the fixed width resolves from the declared type), packed / unpacked (overriding the syntax-level default for a repeated scalar), and, under proto2, default = <literal>. On an enum, #[proto_extension] numbers the variants: a unit enum’s values from 0 (proto3 requires a variant numbered 0), a data-carrying enum’s oneof members from 1; #[proto_extension(enum_type = "open" | "closed")] overrides the openness. The macro validates every number at compile time — positive, unique per message, within 1..=536_870_911, outside the reserved 19000..=19999 — and rejects an id on a skip or flatten field, which protobuf cannot address. A constant entry is refused the same way: a field-numbered format has no slot for an entry with no number.

The readers are field_number(), variant_number(), syntax(), int_encoding(), packed(), and enum_openness(); the type-level #[proto_extension(syntax = "proto2")] bundle switches the presence semantics wholesale — explicit scalar presence, unpacked repeated fields, closed enums, custom defaults — and the per-field keys override it. A Duration field on a gRPC-transcoded API takes the duration(format = "protobuf") rule from the datetime chapter. The capiw_prost codec wraps prost’s fuzzed varint/tag/length-delimited primitives — deliberately not the derive-based prost::Message trait, because a Capi codec consumes the erased event stream, which a derive-generated Message impl cannot speak.

This is the concrete payoff of a static binding: the codec learns every field number from T::BINDING without ever holding a value.

Writing your own

To add a format, you write a contract crate with the three parts above:

  1. Define the marker and impl Format for YourMarker {}. It’s an unsealed, empty trait, so this is one line in your own crate — no central registry, no orphan-rule problem. Check first that a contract for your format doesn’t already exist: a second marker for an existing format splits it into two mutually incompatible formats as far as the compiler is concerned, which is the one thing worth coordinating.
  2. Decide your keys, namespaced ("myfmt.thing"), and write typed readers over them.
  3. If your format needs author-facing annotations, add a decoration macro behind a model feature that lowers them into #[wire(ext(...))] and expands to the core derives. If it doesn’t — if your format is self-describing — skip this entirely and ship just the marker, as JSON does.

Then a byte engine implements ByteEncode and ByteDecodeWireFormat is the blanket over the two — and returns Codec<YourMarker> from a codec() function. ByteEncode::encode_into (append to a caller-owned buffer) has a provided default over encode_u8, and a scalar rule’s render_bytes / parse_bytes are what an engine calls for a leaf with a rule. Nothing in the core changes, and nothing in the core needed to know your format existed.

Next: Hand-Written Impls and the Value Tree — the last resort when a type’s shape defeats every attribute.