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

  1. Make general, pnls, and returns plain JSON objects mapping metric names to values
  2. Use null-safe defaults: emit {} for empty sections rather than null or arrays
  3. Regenerate the document via the library's canonical writer
  4. 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

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


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