nautechsystems/nautilus_trader · error
canonical statistics field '{key}' must be an object
Error message
canonical statistics field '{key}' must be an object What it means
Within the canonical statistics object, the fields `general`, `pnls`, and `returns` must each be JSON objects (maps of metric name to value). This error is thrown when any of these keys holds a different type — array, string, number, or null. It ensures consumers can iterate metric key/value pairs uniformly.
Source
Thrown at crates/backtest/src/result.rs:500
&& !value.starts_with('0')
&& value.bytes().all(|byte| byte.is_ascii_digit())),
"canonical run field '{field}' is not a canonical unsigned decimal"
);
Ok(())
}
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)?;View on GitHub (pinned to 18893faf8b)
Solutions
- Make general, pnls, and returns plain JSON objects mapping metric names to values
- Use null-safe defaults: emit {} for empty sections rather than null or arrays
- Regenerate the document via the library's canonical writer
- Transform array/tuple-based stat representations into objects before validation
Example fix
// before
{"statistics": {"pnls": [["PnL", "100"]], "returns": null}}
// after
{"statistics": {"pnls": {"PnL": "100"}, "returns": {}}} Defensive patterns
Strategy: validation
Validate before calling
fn stat_sections_are_objects(stats: &serde_json::Value) -> bool {
["general", "pnls", "returns"]
.iter()
.all(|k| stats.get(*k).map_or(false, serde_json::Value::is_object))
} Type guard
fn is_metric_map(v: &serde_json::Value) -> bool {
v.is_object() && v.as_object().map_or(false, |m| m.iter().all(|(k, _)| !k.is_empty()))
} Try / catch
match validate_document(&doc) {
Err(e) if e.to_string().contains("must be an object") => {
// convert offending statistics section to an object and retry
}
other => other,
} Prevention
- Serialize metric maps as JSON objects keyed by metric name
- Emit {} for empty sections instead of null or arrays
- Convert tuple/pair lists to maps before canonicalization
- Cover statistics serialization with round-trip tests
When it happens
Trigger: Validating a document where statistics.general, statistics.pnls, or statistics.returns is not an object — e.g. an array of [name, value] pairs, a flattened string, or null for an empty section. Common when statistics were serialized from a different in-memory representation.
Common situations: Exporting metrics as arrays of tuples from Python dicts with non-string keys; nulling out empty stat sections; merging statistics from multiple sources into the wrong shape; older fixture formats.
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/26f33b2b060a0996.
Report an issue: GitHub.