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
- Ensure the `Position` struct's `id` field is serialized as `"id"` (remove any `skip_serializing` or rename attributes).
- Run `cargo test` for the backtest result serialization tests to confirm the serde contract.
- If the field was renamed intentionally, update `canonical_position` to look up the new key and regenerate expected fixtures.
- 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
- Never add skip_serializing to identifier fields on domain types.
- Add a serialization round-trip test asserting `id` is present in JSON output.
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
- serialized order did not contain an object core
- serialized position was not an object
- serialized position adjustment was not an object
- Cannot handle command: {command:?}
- Latency model should be initialized
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/9185be1c28e6cadd.
Report an issue: GitHub.