with_conversion vs with_domain_type

Do not call both for the same selector — domain type already enables conversion.

A with_conversionB with_domain_type
IdeaGeneric convert API; you plug any app typeAlways use this Rust path
build.rs.with_conversion(named_type("Decimal")).with_domain_type(…, "rust_decimal::Decimal")
You writeTryFromSbe<Decimal> / TryToSbe<Decimal> for your typeUsually nothing for bool / rust_decimal / chrono
Decodelet p: Cents = dec.price_as()?let p: rust_decimal::Decimal = dec.try_price()?
Encodeenc.price_from(&cents)?enc.try_price(rust_decimal::Decimal::new(12345, 2))?
Raw wireprice_value() / price_wire(...)same when conversion is active
Sampleexchange-example · demo_conversion_onlyl3-book

Option A — you choose the app type (Cents)

#![allow(unused)]
fn main() {
    use ergo_sbe::{ConversionSelector, GenerationConfig};
    // A — generic converter: one wire type, many app types
    let _cfg =
        GenerationConfig::new("msgs").with_conversion(ConversionSelector::named_type("Decimal"));
}

(From book/examples/conversion-config.rs — a self-contained program that compiles against ergo-sbe.)

#![allow(unused)]
fn main() {
use rust_decimal::Decimal as Rd;
// App adapter: wire Decimal ↔ rust_decimal::Decimal
struct FixedPrice { mantissa: i64, exponent: i8 }

impl TryFromSbe<Decimal> for FixedPrice {
    type Error = &'static str;
    fn try_from_sbe(wire: Decimal) -> Result<Self, Self::Error> {
        Ok(FixedPrice {
            mantissa: wire.mantissa(),
            exponent: wire.exponent(),
        })
    }
}
impl TryToSbe<Decimal> for FixedPrice {
    type Error = &'static str;
    fn try_to_sbe(&self) -> Result<Decimal, Self::Error> {
        Ok(Decimal::new(self.mantissa, self.exponent))
    }
}
}

(From book/examples/conversion-app-code.rs — app adapter pattern, compiles against tour_codec.)

// Encode using the generic conversion API:
let mut buf = [0u8; QuoteEncoder::compute_length_with_header()];
let price = Rd::new(12345, 2); // 123.45
let len = QuoteEncoder::try_wrap_and_apply_header(&mut buf, 0)?
    .price_from(&price)?
    .size_from(&Rd::new(10, 0))?
    .encoded_length_with_header();
// Decode — generic `_as::<T>()` picks your adapter:
let dec = QuoteDecoder::try_from(&buf[..len])?;
let p: Rd = dec.price_as()?;
assert_eq!(p, Rd::new(12345, 2));
// Same buffer, different app type — only possible with with_conversion:
let fixed: FixedPrice = dec.price_as()?;
assert_eq!(fixed.mantissa, 12345);
assert_eq!(fixed.exponent, -2);

(Same file — generic _from/_as encode/decode with with_conversion.)

Option B — one fixed app type

#![allow(unused)]
fn main() {
    use ergo_sbe::{ConversionSelector, GenerationConfig};
    // B — concrete mapping: one Rust type per wire type (already enables conversion)
    let _cfg = GenerationConfig::new("msgs").with_domain_type(
        ConversionSelector::named_type("Decimal"),
        "rust_decimal::Decimal",
    );
}

(Same source file — book/examples/conversion-config.rs.)

// Encode using the generic conversion API:
let mut buf = [0u8; QuoteEncoder::compute_length_with_header()];
let price = Rd::new(12345, 2); // 123.45
let len = QuoteEncoder::try_wrap_and_apply_header(&mut buf, 0)?
    .price_from(&price)?
    .size_from(&Rd::new(10, 0))?
    .encoded_length_with_header();
// Decode — generic `_as::<T>()` picks your adapter:
let dec = QuoteDecoder::try_from(&buf[..len])?;
let p: Rd = dec.price_as()?;
assert_eq!(p, Rd::new(12345, 2));
// Same buffer, different app type — only possible with with_conversion:
let fixed: FixedPrice = dec.price_as()?;
assert_eq!(fixed.mantissa, 12345);
assert_eq!(fixed.exponent, -2);

Both styles on different fields:

pub fn demo_conversion_only() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let mut buf = [0u8; QuoteEncoder::compute_length_with_header()];

    let price = Rd::new(12345, 2); // 123.45
    let size = Rd::new(10, 0);
    let mut enc = QuoteEncoder::try_wrap_and_apply_header(&mut buf, 0)?;
    enc.price_from(&price)?;
    enc.size_from(&size)?;
    let len = QuoteEncoder::compute_length_with_header();

    let dec = QuoteDecoder::try_from(&buf[..len])?;
    let wire = dec.price_value();
    assert_eq!(wire.mantissa(), 12345);
    assert_eq!(wire.exponent(), -2);

    let price2: Rd = dec.price_as()?;
    let size2: Rd = dec.size_as()?;
    assert_eq!(price2, price);
    assert_eq!(size2, size);

    // Same buffer, different app type — only possible with with_conversion.
    let fixed: FixedPrice = dec.price_as()?;
    assert_eq!(
        fixed,
        FixedPrice {
            mantissa: 12345,
            exponent: -2
        }
    );

    let dto = QuoteDomain::try_from_decoder(dec)?;
    assert_eq!(dto.price.mantissa(), 12345);
    let mut re = [0u8; QuoteEncoder::compute_length_with_header()];
    let n = dto.encode(&mut re)?;
    assert_eq!(&re[..n], &buf[..len]);
    Ok(buf[..len].to_vec())
}

(This code comes from the sbe-feature-tour sample crate.)

Option B, manual impl — concrete signatures, your own conversion logic

with_domain_type(selector, path) is the common case: ergo-sbe also generates the TryFromSbe/TryToSbe impl for bool / rust_decimal::Decimal / chrono::DateTime<Utc>. If you need different conversion behaviour for one of those exact three types — a custom rounding rule, stricter validation, different null handling — call additive with_manual_domain_type instead: same generated try_price(...)? / try_price()? signatures, but you write the impl:

    let (_schema, src) = generate_domain_with(&path, "manual_impl_dt", |c| {
        c.with_manual_domain_type(
            ergo_sbe::ConversionSelector::named_type("Decimal"),
            "rust_decimal::Decimal",
        )
    });
        use rust_decimal::Decimal;

        // Caller-supplied impl: deliberately scales mantissa by 10 on the way
        // in/out, so a value only round-trips correctly through THIS impl —
        // proof ergo-sbe didn't quietly generate its own.
        impl TryFromSbe<self::Decimal> for rust_decimal::Decimal {
            type Error = &'static str;
            fn try_from_sbe(wire: self::Decimal) -> Result<Self, Self::Error> {
                Ok(rust_decimal::Decimal::new(wire.mantissa() / 10, (-wire.exponent()) as u32))
            }
        }
        impl TryToSbe<self::Decimal> for rust_decimal::Decimal {
            type Error = &'static str;
            fn try_to_sbe(&self) -> Result<self::Decimal, Self::Error> {
                Ok(self::Decimal::new(self.mantissa() as i64 * 10, -(self.scale() as i8)))
            }
        }

        let mut buf = [0u8; 256];
        let mut enc = OrderEncoder::wrap_and_apply_header(&mut buf, 0)
            .fixed(&OrderFixedFields { price: self::Decimal::new(0, 0), size: self::Decimal::new(0, 0) });
        enc.try_price(Decimal::new(12345, 2))?;
        enc.try_size(Decimal::new(100, 0))?;
        let encoded = enc.as_bytes_with_header().to_vec();

        let dec = OrderDecoder::try_decode(&encoded, 0)?;
        assert_eq!(dec.try_price()?, Decimal::new(12345, 2));
        assert_eq!(dec.try_size()?, Decimal::new(100, 0));

(From domain_type_manual_impl_uses_callers_own_impl in sbe/tests/baseline_test.rs — a real generated-and-compiled test. Any rust_type string that isn't one of the three built-ins never gets an auto-generated impl regardless of DomainImpl — it only matters for opting those three in or out.)

Forgot the impl? Two things soften it. First, the compile error names the missing impl directly instead of the default trait-bound message:

error[E0277]: `rust_decimal::Decimal` has no `TryFromSbe<Decimal>` impl
  |
  | missing `impl TryFromSbe<Decimal> for rust_decimal::Decimal`
  |
  = note: if this field uses DomainImpl::Manual, the generated `try_*`
          accessor's doc comment has a ready-to-paste starting point

Second, for the three built-ins, that pointer is real: the generated try_price method's own doc comment (visible on hover, or in cargo doc) carries the exact impl DomainImpl::Generated would have written — copy it out and adjust. sbe/tests/baseline_test.rs's domain_type_manual_impl_doc_comment_has_generated_snippet asserts this snippet is present in the generated source.