nautechsystems/nautilus_trader · error

serialized enum variant was not an object

Error message

serialized enum variant was not an object

What it means

Raised by `variant_payload_mut`, which expects a serialized enum (externally tagged) as `{ "VariantName": { ...payload... } }`. It grabs the first value of the outer object and requires it to be an object; if the variant payload is an array, string, or number, the canonicalizer cannot patch decimals and fails. Callers include `canonical_order`, `canonical_position`, `canonical_account`, and `patch_order_event_decimals`.

Source

Thrown at crates/backtest/src/result.rs:762

}

fn patch_position_adjustment(
    adjustment: &PositionAdjusted,
    value: &mut Value,
) -> anyhow::Result<()> {
    let object = value
        .as_object_mut()
        .ok_or_else(|| anyhow::anyhow!("serialized position adjustment was not an object"))?;
    set_decimal(object, "quantity_change", adjustment.quantity_change);
    Ok(())
}

fn variant_payload_mut(value: &mut Value) -> anyhow::Result<&mut Map<String, Value>> {
    let variant = value
        .as_object_mut()
        .and_then(|object| object.values_mut().next())
        .and_then(Value::as_object_mut)
        .ok_or_else(|| anyhow::anyhow!("serialized enum variant was not an object"))?;
    Ok(variant)
}

fn canonical_value<T: Serialize>(source: &T) -> anyhow::Result<Value> {
    let mut value = serde_json::to_value(source)?;
    canonicalize_value(&mut value)?;
    Ok(value)
}

fn canonicalize_value(value: &mut Value) -> anyhow::Result<()> {
    value.sort_all_objects();
    sort_named_arrays(value, false);
    stringify_numbers(value)?;
    Ok(())
}

fn canonicalize_document(value: &mut Value) -> anyhow::Result<()> {
    value.sort_all_objects();

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Use only struct variants (externally tagged with object payloads) in enums fed to canonicalization.
  2. If the variant is a newtype wrapping a struct, wrap it so the payload is `"Variant": { field: ... }`.
  3. Verify with `serde_json::to_value(&event)` that the top level is `{ "Name": { ... } }`.
  4. Extend `variant_payload_mut` to handle the new representation only if the schema change is intentional.

Example fix

// before: OrderFilled(Quantity)  // -> {"OrderFilled": 5}
// after: OrderFilled { quantity: Quantity }  // -> {"OrderFilled": {"quantity": ...}}
Defensive patterns

Strategy: type-guard

Validate before calling

let v = serde_json::to_value(&event)?;
assert!(v.as_object().and_then(|o| o.values().next()).map(Value::is_object).unwrap_or(false));

Type guard

fn has_object_variant_payload(v: &serde_json::Value) -> bool {
    v.as_object()
        .and_then(|o| o.values().next())
        .map(Value::is_object)
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: Canonicalizing any order/position/account event enum whose variant payload serializes as a non-object — tuple variants, newtype variants wrapping primitives/arrays, or unit variants (which serialize to a bare string).

Common situations: Adding a new unit or tuple variant to an event enum used in backtest results; changing an enum from struct to tuple representation; deserializing an unexpected type into the canonicalizer.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/0289b0d7d2581ab6. Report an issue: GitHub.