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.)