App Types on Composites
SBE composite types like Decimal { mantissa, exponent } have a fixed wire
layout. Mapping them to Rust domain types is done at configuration time — no
hand-rolled per-field converters needed.
Option A — generic converter (with_conversion): the generated codec
emits price_from<T: TryToSbe<Decimal>>() and price_as<T: TryFromSbe<Decimal>>().
You implement the trait for any app type. One wire type, many app types.
Option B — concrete mapping (with_domain_type): the generated codec
emits try_price(rust_decimal::Decimal)? and try_price()? -> rust_decimal::Decimal.
Exactly one Rust type per wire type. The trait impls are generated for you.
Full comparison with code examples: with_conversion vs with_domain_type.
Optional composites with a null image
A composite member itself can carry presence="optional" with a schema
nullValue — e.g. a PriceNull9-style Decimal whose mantissa is optional
while exponent stays constant. That member decodes as Option<i64>
(checked against the wire null sentinel), not a bare i64. This is distinct
from a composite field gated by sinceVersion (which the whole composite
accessor wraps in Option<Decoder>), and from a composite with no nullValue
anywhere (which has no null image to check, so it decodes as a plain value —
see with_conversion vs with_domain_type).
Because the wire null sentinel is not a valid rust_decimal::Decimal, the
with_domain_type accessor fails closed with a typed error rather than
silently decoding the sentinel as a huge/wrong number:
use rust_decimal::Decimal;
// `price: None` writes the schema null image (mantissa = nullValue).
let mut buf = [0u8; QuoteEncoder::compute_length_with_header()];
let len = QuoteEncoder::wrap_and_apply_header(&mut buf, 0)
.fixed(&QuoteFixedFields { price: None, qty: 7 })
.encoded_length_with_header();
let dec = QuoteDecoder::try_decode(&buf[..len], 0)?;
assert_eq!(dec.price_value().mantissa(), None);
assert!(dec.try_price().is_err(), "null mantissa is not a valid Decimal");
// A real price round-trips through the null-aware member accessor.
let len2 = QuoteEncoder::wrap_and_apply_header(&mut buf, 0)
.fixed(&QuoteFixedFields { price: None, qty: 7 })
.try_price(Decimal::new(12345, 9))?
.encoded_length_with_header();
let dec2 = QuoteDecoder::try_decode(&buf[..len2], 0)?;
assert_eq!(dec2.price_value().mantissa(), Some(12345));
assert_eq!(dec2.try_price()?, Decimal::new(12345, 9));
(From optional_composite_member_null_image_roundtrip in
sbe/tests/baseline_test.rs — a real generated-and-compiled test, not a
standalone snippet.)