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
- Serialize returns_series as a JSON array of entries preserving order
- Convert timestamp-keyed maps to arrays before validation
- Emit [] for an empty series instead of null
- 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
- Store return samples in ordered collections (Vec/array), not keyed maps
- Serialize empty series as [] not null
- Preserve sample order through any transformations
- Add a validator run to the result export pipeline
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
- canonical backtest result must be a JSON object
- canonical result field '{key}' must be an array
- canonical result summary must be an object
- canonical run configuration ID must be a string or null
- canonical trader ID must be a string
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/41b1b2a05891b539.
Report an issue: GitHub.