Every piece of data that crosses the Binions bus is a typed Rust value with a generated JSON Schema. The envelope, the four-kind discriminator, and every operation payload all derive their schema automatically, so the same definition is the source of truth for the running code, the serialized JSON on the wire, and the published contract. This page describes those data models and how they evolve safely over time.
Schemas are derived, not hand-written. Binions uses the
schemarslibrary: a Rust type annotated with#[derive(JsonSchema)]produces a JSON Schema directly from its fields. There is no separate schema file to keep in sync — the type is the schema.
The outermost model is the event envelope. It derives serialization and JSON Schema, so a machine-readable schema exists for the header itself. In Rust it is generic over the payload:
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EventEnvelope<P> {
pub event_id: Uuid, // v7, time-sortable
pub event_kind: Kind, // Action | Fact | Log | Control
pub event_type: String, // "<Domain>.<Name>"
pub event_version: u32,
pub producer: String,
pub target_service: Option<String>,
pub timestamp: DateTime<Utc>,
pub correlation_id: Option<Uuid>,
pub causation_id: Option<Uuid>,
pub aggregate_id: Option<String>,
pub metadata: HashMap<String, String>,
pub payload: P,
}
Optional fields use "skip if empty" serialization, so a fact without a correlation id simply omits that key rather than writing null.
The discriminator is a four-value enum serialized in PascalCase. It also derives JSON Schema, so the allowed values are part of the published contract:
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum Kind { Action, Fact, Log, Control }
Each operation defines its own payload struct. These are ordinary serde structs that also derive JsonSchema; the daemon framework requires it, so every action and fact a daemon handles has a schema. A minimal example:
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct OrderPlaced {
pub order_id: String,
pub total_pennies: u64,
}
On the wire this becomes the payload object inside the envelope. The pairing of event_type + event_version tells a consumer which payload schema to expect. The full set of message schemas is published in the AsyncAPI document (90 messages plus shared components).
Payloads change over time. Binions handles this with explicit versioning rather than guesswork:
event_version (which defaults to 1).// promote an Order.Placed payload from v1 to the current version
let current = registry.upcast("Order.Placed", from_version, target_version, payload)?;
Downgrading is intentionally unsupported — the platform only moves payloads forward.
Two layered error types make failures explicit and typed rather than stringly-typed. Envelope construction and serialization raise an EventError; the daemon framework raises a MicroserviceError. Selected variants:
| Type | Representative variants |
|---|---|
EventError | MissingField, InvalidEventType, MissingTargetService, InvalidProducer, EmptyAggregateId, UpcastFailed |
MicroserviceError | Config, RedisConnect, PayloadDecode, SchemaValidation, HandlerFailure, DuplicateHandler, AxumBind |
Why typed errors matter. A
PayloadDecodeerror names theevent_typeand version that failed to parse; aDuplicateHandlererror is caught at startup, not in production. The types turn whole classes of bug into compile-time or boot-time failures.