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

Scalars, Numbers, and Money

Leaf values are where wire bugs hide: a number quoted as a string, bytes that should have been base64, money rounded through a float. The wire model handles each with a scalar rule — one per field, declared on the field, checked against its type at derive time.

Numbers

Some APIs send numbers as JSON strings ("1736380800"). Model the field as its real numeric type and annotate the wire form; never change the Rust type to String to match the wire.

#[wire(number(as = "string"))] pub amount_cents: i64,   // reads/writes "12345"

number(...) takes three knobs:

KnobEffect
as = "string" / as = "number"a quoted string on the wire, or the native number lane (the default)
scale = Na fixed decimal scale
rounding = "..."how a precision-dropping encode rounds: half_even (the default), half_up, half_down, up, down, ceil, floor, or unnecessary, which refuses a value that would need rounding

Bytes

A Bytes field renders as an encoded string with one of three rules:

#[wire(base64)]    pub data: Bytes,
#[wire(base64url)] pub token: Bytes,
#[wire(hex)]       pub digest: Bytes,

The distinction between Bytes and Vec<u8> matters and is easy to trip over: in the wire model’s core shape, Vec<u8> is a repeated integer — a JSON array of numbers, and in protobuf a repeated field. Bytes is a genuine byte string. Reach for Bytes whenever the value is opaque binary data — and give it a rule: with none set, a text codec renders a Bytes field as an array of byte values, which is rarely what the service wants. Sentinel mapping applies to leaves of every kind: none_when turns a service’s -1 or "" into None on a numeric or string field as readily as on a date — the datetime chapter shows it.

Joined lists

A Vec of scalar leaves can render as one separator-joined string:

#[wire(joined = ",")] pub kinds: Vec<Kind>,   // kinds=a,b,c

The separator is a single character, it must not appear inside an element, and a Vec containing a single empty string is rejected on encode. This is common in header and query values — for example a typed field mask:

#[endpoint(location(header))]
#[wire(rename = "x-goog-fieldmask", joined = ",")]
pub field_mask: Vec<FieldMask<SearchNearbyResponse>>,

FieldMask<T> (from google_places) is a phantom-typed newtypeFieldMask<T>(Cow<'static, str>, PhantomData<fn() -> T>) — so a field mask is tied at compile time to the response type it selects into, catching a mask that names a field the response doesn’t have. The #[endpoint(location(header))] half is endpoint routing, covered in the endpoint contract; the joined half is the wire rule.

Money and decimals

Never model money as f64. Two supported approaches:

Integer minor units. An i64 count of cents plus number(as = "string") when the API quotes it. This is what grok does, and it needs no extra types.

The decimal carriers. capi_wire_decimal provides two types that derive the wire traits and just work as field types — no #[wire] key needed:

TypeShape
Decimalsigned 128-bit coefficient with a per-value power-of-ten exponent; any scale, exact to ~38 significant digits
FixedDecimal<const SCALE: u8>the same with the scale pinned in the type — FixedDecimal<2> for a two-place money field, the way a SQL NUMERIC(_, s) column fixes it

Both parse, format, and rescale but do no arithmetic. That is deliberate: Rust has no core decimal type, and hard-coding a third-party one into an API client would tie every downstream user to that choice. Instead they are carriers that know how to cross the wire, and a third-party decimal crate plugs in by implementing ToWireDecimal/FromWireDecimal for its own type. The framework never depends on a decimal-math library, and a value still round-trips exactly.

Both encode on the raw-number lane, so through a JSON codec a decimal is a bare number preserved to the digit; add #[wire(number(as = "string"))] to send it quoted instead.

Value classes

Two more attributes say what kind of value a field holds rather than how it is spelled: #[wire(sensitive)] for a secret and #[wire(volatile)] for a value that varies run to run. They have no effect on the bytes a codec writes — they are properties recorded in the binding — but they drive the recorder’s masking, the placeholder a fixture stores, and the typed redaction transform a request goes through to be born without its secrets. Value Classes and Redaction is the whole story.

Where the scalar rules stop

One scalar rule per field, and the datetime families own their field’s leaf conversion outright — so #[wire(timestamp(...))] and friends do not combine with number, base64, or joined. They are the next chapter, and they are the largest attribute family in the model for a good reason: a silent wrong unit or offset is the costliest wire bug there is.

For a leaf whose format no rule expresses, write a WireScalar type or route the field through a via proxy.