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:
-
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. TheNullValapproach gives you a plainEventCodetype — if you care about null, checkcode == EventCode::NullVal; if you don't, just use it. (The wire encoding itself would be compatible either way:Nonemaps to the null sentinel,Some(v)maps tov. The issue is ergonomics, not wire format.) -
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. -
Schema intent. The schema declares a null sentinel as part of the enum's own value domain, not as a separate presence flag. A
NullValvariant 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, wire-compatible with sbe-tool, and bears no
runtime cost:
// 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 _bool() accessor alongside the
standard enum getter:
// Standard getter — returns the enum variant (raw wire discriminant).
pub fn available_wire(&self) -> BooleanType { … }
// Null-aware — rejects NullVal (returns Err); Ok(true/false) otherwise.
// Required fields → Result<bool, DecodeError>; optional → Option<bool>.
pub fn try_available_bool(&self) -> Result<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.