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

Why NullVal Instead of Option

An SBE enum may declare a nullValue in the schema — an explicit wire sentinel that means “not present” / “not set”. When the schema doesn’t specify one, SBE defaults to the encoding type’s null sentinel: the maximum value for unsigned types (e.g. 255 for uint8) and the minimum value for signed types (e.g. -128 for int8).

nullValue / minValue / maxValue on a type or field must fit the declared primitive width. nullValue="256" on uint8 is a parse error — it would otherwise collapse to 0 on the wire and make Some(0) indistinguishable from None. See Error Diagnostics.

An early design tried wrapping every enum field in Option<EventCode> at the field site:

// Option approach — REJECTED
pub fn event_code(&self) -> Option<EventCode> { … }
pub fn set_event_code(&mut self, val: Option<EventCode>) { … }

This was rejected for three reasons:

  1. API complexity. Using Option<EventCode> at every access point forces every consumer to .unwrap() or match, even when the field is known to be populated. The NullVal approach gives you a plain EventCode type — if you care about null, check code == EventCode::NullVal; if you don’t, just use it. (The wire encoding itself would be compatible either way: None maps to the null sentinel, Some(v) maps to v. The issue is ergonomics, not wire format.)

  2. Generated code complexity. Every field site that uses Option<EventCode> needs value↔Option mapping in both accessor directions, inflating the generated code for no wire-format gain.

  3. Schema intent. The schema declares a null sentinel as part of the enum’s own value domain, not as a separate presence flag. A NullVal variant reflects that intent directly in the Rust type.

The chosen design adds a NullVal variant to every generated enum. It is the same size as any other variant and wire-compatible with sbe-tool. Its runtime cost is not measured here:

// ergo-sbe generated (conceptual)
pub enum EventCode {
    NullVal = 255,  // or schema-declared nullValue
    Ok = 200,
    Error = 400,
    Timeout = 408,
}

For an Optional field (schema presence="optional"), the generated accessor returns Option<EventCode> — but the null check compares against the NullVal discriminant on the wire, never allocates, and is transparent to the caller.

On encode, fixed(&FixedFields) always writes every fixed field: Some(v) writes the value and None writes the exact schema null wire image (including nested optional composite members). Dirty buffer reuse is therefore safe when you go through fixed. Prefer that path for whole-message encodes.

apply_nulls() remains on the unfixed encoder after wrap_and_apply_header when you set individual optional fields piecemeal instead of using FixedFields. See Encode and Decode.

Opting into Option<T> with with_null_as_option

The NullVal is the right default, but some codebases prefer Option throughout. Use with_null_as_option to make generated enum accessors return Option<Enum> — NullVal maps to None, all other values to Some(v). Wire bytes are identical either way.

use ergo_sbe::{ConversionSelector, GenerationConfig};

// Individual enum → Option<Enum>
let config = GenerationConfig::new("msgs")
    .with_null_as_option(ConversionSelector::named_type("EventCode"));

// Every enum in the schema → Option<Enum>
let config = GenerationConfig::new("msgs")
    .with_all_enums_as_option();

Generated diff (individual setter):

// Default (NullVal)                         // with_null_as_option
pub fn code(&self) -> EventCode { … }   →   pub fn code(&self) -> Option<EventCode> { … }

The as_option() method is also generated on every enum for manual use: event_code.as_option() → Option<EventCode>.

Null-aware accessors on BooleanType

For BooleanType fields, ergon emits a {field}_bool() accessor alongside the standard enum getter. One name at every shape and location, the same name as the {field}_bool(v) encoder setter; it returns Result because the decode is fallible:

// Standard getter — returns the enum variant (raw wire discriminant).
pub fn available(&self) -> BooleanType { … }

// Null-aware — rejects NullVal (returns Err); Ok(true/false) otherwise.
pub fn available_bool(&self) -> Result<bool, DecodeError> { … }

// A `sinceVersion > 0` field can also be absent, so its getter adds the
// `Option` layer — the only shape difference, at message and group level:
pub fn added_later_bool(&self) -> Result<Option<bool>, DecodeError> { … }

For enums and other types, the NullVal variant remains the default. with_null_as_option (above) is the opt-in Option<T> mapping; the wire encoding is identical either way.

Generated enum mappings

with_enum_mapping maps an SBE enum onto an application enum that has no NullVal (see App Types). There, NullVal is never turned into a variant: it decodes as sbe_rt::MappingError::NullEnum, or as None when the enum is also under with_null_as_option. Because the decoder folds an undeclared discriminant into NullVal, an unknown value reaches the mapping as that same NullVal.