nautechsystems/nautilus_trader · error

serialized position did not contain its identifier

Error message

serialized position did not contain its identifier

What it means

Raised by `canonical_position` in the backtest results canonicalizer. After serializing a `Position` to JSON, the function removes the numeric `id` field and replaces it with `position_id` (the string form); if the serialized object lacks `id`, the invariant that every position carries an identifier is broken and the canonicalization fails via `anyhow::ensure!`. This guards the round-trip contract that downstream result consumers rely on for stable position identification.

Source

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

            }
            let mut encoded = serde_json::to_value(event)?;
            canonicalize_value(&mut encoded)?;
            fills.push(json!({
                "client_order_id": order.client_order_id().to_string(),
                "event": encoded,
                "order_event_ordinal": ordinal.to_string(),
            }));
        }
    }
    Ok(fills)
}

fn canonical_position(position: &Position) -> anyhow::Result<Value> {
    let mut value = serde_json::to_value(position)?;
    let object = value
        .as_object_mut()
        .ok_or_else(|| anyhow::anyhow!("serialized position was not an object"))?;
    anyhow::ensure!(
        object.remove("id").is_some(),
        "serialized position did not contain its identifier"
    );
    object.insert(
        "position_id".to_string(),
        Value::String(position.id.to_string()),
    );
    set_f64(object, "avg_px_close", position.avg_px_close);
    set_f64(object, "avg_px_open", Some(position.avg_px_open));
    set_f64(object, "realized_return", Some(position.realized_return));
    set_f64(object, "signed_qty", Some(position.signed_qty));

    if let Some(adjustments) = object.get_mut("adjustments").and_then(Value::as_array_mut) {
        for (source, encoded) in position.adjustments.iter().zip(adjustments) {
            patch_position_adjustment(source, encoded)?;
        }
    }

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure the `Position` struct's `id` field is serialized as `"id"` (remove any `skip_serializing` or rename attributes).
  2. Run `cargo test` for the backtest result serialization tests to confirm the serde contract.
  3. If the field was renamed intentionally, update `canonical_position` to look up the new key and regenerate expected fixtures.
  4. Check for stale generated bindings (cython/Rust codegen) mismatching the current `Position` schema and rebuild.

Example fix

// before: #[serde(skip_serializing)] pub id: PositionId
// after:
#[serde(rename = "id")]
pub id: PositionId
Defensive patterns

Strategy: validation

Validate before calling

let value = serde_json::to_value(&position)?;
assert!(value.get("id").is_some(), "Position must serialize its id");

Type guard

fn has_position_id(v: &serde_json::Value) -> bool {
    v.as_object().map(|o| o.contains_key("id")).unwrap_or(false)
}

Prevention

When it happens

Trigger: Calling the backtest result canonicalization path (e.g. when producing JSON output from `Position`) with a `Position` whose serde serialization omits the `id` field — typically `#[serde(skip)]`/`skip_serializing_if` on `id`, a custom `Serialize` impl that drops it, or a field renamed away from `id`.

Common situations: Upgrading the domain `Position` type after a nautilus_core refactor renames or moves the `id` field; hand-rolled test doubles or shim types implementing `Serialize` without `id`; feature-flagged serialization changes in the position model.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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