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

Multi-Schema Patterns

SBE schemas often share types (messageHeader, groupSizeEncoding, composites, enums, sets). ergo-sbe supports two approaches:

ApproachWhenMethod
xi:include (standard)Schema files live together; official SBE portability matters<include href="common-types.xml"/> — parse_file resolves includes relative to the base dir
Shared Ir (programmatic)Schemas are parsed from strings, generated, or live in separate repos; no filesystem dependencyparse_with_shared / parse_file_with_shared — seed one parse from another’s resolved types

The <include> path is what the SBE spec expects. The shared-Ir path is a convenience for tooling, build scripts, and any workflow where you already have the shared schema parsed in memory.

Shared Ir — parse, then share

// 1. Parse the shared schema once (composites, enums, sets).
let common = ergo_sbe::parse_file("schemas/common-types.xml")?;

// 2. Parse a consumer schema — no <include> needed.
let orders = ergo_sbe::parse_file_with_shared("schemas/orders.xml", &common)?;

// 3. Each schema gets its own module.
let generator = ergo_sbe::Generator::new(
    ergo_sbe::GenerationConfig::new("common_types").with_shared_module("common_types"),
);
let modules = generator.generate_multi(&[
    (&ergo_sbe::Schema::from_ir(common), "common_types"),
    (&ergo_sbe::Schema::from_ir(orders), "orders"),
])?;

With with_shared_module("common_types"), the first entry owns the shared enums/sets/composites; later entries pub use super::common_types::* and skip duplicate type generation.

Module names must be unique, non-empty Rust identifiers (no path separators or keywords). Before any file is written, shared types with the same name are compared by a canonical wire fingerprint (token order, primitive encodings, offsets, presence, null/min/max, discriminants/choices, sinceVersion, and schema byte order). A name collision with a different fingerprint fails generation with GenerateError::IncompatibleSharedType rather than silently reusing the first schema’s layout.

parse_with_shared from in-memory strings

let common = ergo_sbe::parse(
    r#"<?xml version="1.0"?>
<messageSchema package="common" id="0" version="1" byteOrder="littleEndian">
  <types>
    <composite name="messageHeader">
      <type name="blockLength" primitiveType="uint16"/>
      <type name="templateId" primitiveType="uint16"/>
      <type name="schemaId" primitiveType="uint16"/>
      <type name="version" primitiveType="uint16"/>
    </composite>
    <composite name="Price">
      <type name="mantissa" primitiveType="int64"/>
      <type name="exponent" primitiveType="int8"/>
    </composite>
  </types>
</messageSchema>"#,
)?;

// No <types> / <include> — Price resolves from `common`.
let orders = ergo_sbe::parse_with_shared(
    r#"<?xml version="1.0"?>
<messageSchema package="orders" id="1" version="1" byteOrder="littleEndian"
               headerType="messageHeader">
  <message name="NewOrder" id="1">
    <field name="price" id="1" type="Price"/>
  </message>
</messageSchema>"#,
    &common,
)?;

The shared Ir path does not recover bare top-level <type> typedefs (those are inlined during parsing and dropped from the token stream). Reference them through a <composite> / <enum> / <set> in the shared schema instead.

Full build.rs — one helper call

Module names are supplied, not derived from file stems, so a hyphenated common-types.xml can emit common_types.rs. The helper parses the shared schema first, resolves consumers against it, validates the whole set, then writes. A late consumer failure leaves no partial files. Cargo watches every root and every resolved include.

// build.rs
use std::path::Path;
use ergo_sbe::{GenerationConfig, SchemaFile, generate_multi_to_out_dir};

fn main() -> ergo_sbe::miette::Result<()> {
    generate_multi_to_out_dir(
        SchemaFile::new(Path::new("schemas/common-types.xml"), "common_types"),
        &[
            SchemaFile::new(Path::new("schemas/orders.xml"), "orders"),
            SchemaFile::new(Path::new("schemas/fills.xml"), "fills"),
        ],
        GenerationConfig::new("common_types"),
    )?;
    Ok(())
}

Consumer modules import the shared module for cross-schema type resolution:

mod common_types { include!(concat!(env!("OUT_DIR"), "/common_types.rs")); }
mod orders {
    use super::common_types::*;       // shared types + header composite
    include!(concat!(env!("OUT_DIR"), "/orders.rs"));
}
mod fills {
    use super::common_types::*;
    include!(concat!(env!("OUT_DIR"), "/fills.rs"));
}

AnyMessage is generated once per schema module. Template ids are not unique across schemas. Generate an opt-in AnySchemaMessage over every configured schema with Generator::generate_schema_dispatch:

#![allow(unused)]
fn main() {
use ergo_sbe::{GenerateError, GeneratedModule, GenerationConfig, Generator, Schema};

fn dispatcher(market: &Schema, trading: &Schema) -> Result<GeneratedModule, GenerateError> {
    Generator::new(GenerationConfig::new("market")).generate_schema_dispatch(
        &[(market, "market"), (trading, "trading")],
        "any_schema",
    )
}
}

Write the returned source to its path beside the codec modules, then include any_schema.rs in a sibling module. This also works when the codecs were generated separately with different conversion settings. Each codec must have AnyMessage dispatch enabled. The combined enum wraps it in a variant named from the module, such as Market and Trading, and reads schemaId before templateId. Decode errors retain the originating schema’s error type.

Schema ids and module names must be unique. Headers must declare the same wire layout and byte order, with a required, encoded schemaId. Custom layouts and big-endian headers work; mixed layouts or byte orders are rejected as ambiguous. Single-schema generation and the ordinary codec hot paths are unchanged.

Pass a slice bounded to one transport frame to AnySchemaMessage::decode. An unknown schema id returns Other { schema_id, template_id, frame }. An unknown template in a known schema remains that module’s AnyMessage::Unknown. Both retain the supplied header and body. as_bytes() borrows the complete frame without allocating, including future-version fields and unknown frames, for persistence or forwarding.

See also: sbe-codegen-examples (reusable generator setup), multi_schema_versioning_test (versioned schemas with shared types), exchange-example (multi-schema exchange feed with IPC).