nautechsystems/nautilus_trader · error

serialized position was not an object

Error message

serialized position was not an object

What it means

`canonical_position` serializes a Position to JSON and expects the top-level value to be a JSON object so it can rename `id` to `position_id` and patch decimal fields. This error is thrown when serde_json::to_value(position) yields a non-object (array, string, number, null), which cannot happen for the library's own Position struct — making this an internal invariant check against serialization drift or misuse.

Source

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

                continue;
            }
            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. Serialize the standard library Position type directly, with no wrapper or custom Serialize impl
  2. Align crate versions so the Position serde format matches what canonical_position expects
  3. Inspect serde_json::to_value(position) to confirm it yields a top-level object
  4. Do not transform the serialized value before passing it through canonicalization

Example fix

// before (custom newtype serialization)
struct PosRef(u64); // serializes as a number
// after
serde_json::to_value(&position)? // standard Position -> top-level object
Defensive patterns

Strategy: try-catch

Validate before calling

fn position_serializes_as_object(position: &Position) -> anyhow::Result<bool> {
    Ok(serde_json::to_value(position)?.is_object())
}

Type guard

fn is_json_object(v: &serde_json::Value) -> bool { v.is_object() }

Try / catch

let canonical = canonical_position(&position).map_err(|e| {
    if e.to_string().contains("was not an object") {
        // inspect serde_json::to_value(position) and crate versions
    }
    e
})?;

Prevention

When it happens

Trigger: Canonicalizing a Position whose serde output is not a JSON object — e.g. the type was swapped for a wrapper/newtype, a custom Serialize impl returns a scalar, or a different Position type/version was passed in. Also triggered if serialization itself was intercepted and transformed.

Common situations: Version mismatch between crates producing and consuming Position serde formats; a custom Serialize/with-wrapper implementation around Position; passing a similarly-named type from another module into the result canonicalizer.

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/c38ec48cf9d216ff. Report an issue: GitHub.