nautechsystems/nautilus_trader · error

canonical returns series must be an array

Error message

canonical returns series must be an array

What it means

The `returns_series` field of the canonical statistics object must be a JSON array (an ordered series of return samples). This error is thrown when it is any other JSON type — object, string, number, or null. Unlike the other statistics fields which are objects, returns_series is inherently ordered, hence the array requirement.

Source

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

}

fn validate_statistics(value: &Value) -> anyhow::Result<()> {
    let object = value
        .as_object()
        .ok_or_else(|| anyhow::anyhow!("canonical result statistics must be an object"))?;
    validate_fields(
        object,
        &["general", "pnls", "returns", "returns_series"],
        "canonical result statistics",
    )?;

    for key in ["general", "pnls", "returns"] {
        anyhow::ensure!(
            object.get(key).is_some_and(Value::is_object),
            "canonical statistics field '{key}' must be an object"
        );
    }
    anyhow::ensure!(
        object.get("returns_series").is_some_and(Value::is_array),
        "canonical returns series must be an array"
    );
    Ok(())
}

fn optional_nanos(value: Option<UnixNanos>) -> Value {
    value.map_or(Value::Null, |nanos| Value::String(nanos.to_string()))
}

fn canonical_order(order: &OrderAny) -> anyhow::Result<Value> {
    let mut value = serde_json::to_value(order)?;
    let payload = variant_payload_mut(&mut value)?;
    let core = payload
        .get_mut("core")
        .and_then(Value::as_object_mut)
        .ok_or_else(|| anyhow::anyhow!("serialized order did not contain an object core"))?;
    set_decimal(core, "avg_px", order.avg_px());

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Serialize returns_series as a JSON array of entries preserving order
  2. Convert timestamp-keyed maps to arrays before validation
  3. Emit [] for an empty series instead of null
  4. Regenerate the canonical document with the current library version

Example fix

// before
{"statistics": {"returns_series": {"1720000000000000000": "0.01"}}}
// after
{"statistics": {"returns_series": [{"ts_init": "1720000000000000000", "value": "0.01"}]}}
Defensive patterns

Strategy: validation

Validate before calling

fn returns_series_is_array(stats: &serde_json::Value) -> bool {
    stats.get("returns_series").map_or(false, serde_json::Value::is_array)
}

Type guard

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

Try / catch

match validate_document(&doc) {
    Err(e) if e.to_string().contains("returns series must be an array") => {
        // convert map/none series to an array and retry
    }
    other => other,
}

Prevention

When it happens

Trigger: Validating a document where statistics.returns_series is an object (e.g. a map of timestamp->value), null, or a scalar instead of an array. Occurs when series data was serialized as a dict keyed by timestamp or when empty series were encoded as null.

Common situations: Series built from Python/JS dicts (keyed by timestamp) then serialized as objects; empty series encoded as null; schema drift from an older format that used an object map for the series.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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