nautechsystems/nautilus_trader · error

canonical run field '{field}' must be a decimal string

Error message

canonical run field '{field}' must be a decimal string

What it means

`validate_unsigned_decimal` requires canonical run numeric fields (iterations, total_events, total_orders, total_positions) and timestamp fields (backtest_start_ns, backtest_end_ns) to be encoded as JSON strings containing decimal digits. This error is thrown when the field's JSON value is not a string at all (e.g. a number or null for a non-nullable field). The library encodes large integers as strings to preserve exact values.

Source

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

        "canonical trader ID must be a string"
    );
    anyhow::ensure!(
        matches!(
            object.get("outcome").and_then(Value::as_str),
            Some("completed" | "failed" | "incomplete" | "stopped")
        ),
        "unsupported canonical run outcome"
    );
    Ok(())
}

fn validate_unsigned_decimal(value: &Value, field: &str, nullable: bool) -> anyhow::Result<()> {
    if nullable && value.is_null() {
        return Ok(());
    }
    let value = value
        .as_str()
        .ok_or_else(|| anyhow::anyhow!("canonical run field '{field}' must be a decimal string"))?;
    anyhow::ensure!(
        value == "0"
            || (!value.is_empty()
                && !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",

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Encode the field as a decimal string, e.g. "42" instead of 42
  2. Regenerate the document via the library's canonical result writer (which applies string encoding)
  3. Pre-transform the JSON to stringify numeric fields before validation
  4. Check the nullable flag: null is only allowed for backtest_start_ns/backtest_end_ns

Example fix

// before
{"run": {"iterations": 100, "backtest_start_ns": null}}
// after
{"run": {"iterations": "100", "backtest_start_ns": "1720000000000000000"}}
Defensive patterns

Strategy: validation

Validate before calling

fn run_decimal_fields_are_strings(run: &serde_json::Value) -> bool {
    ["iterations", "total_events", "total_orders", "total_positions",
     "backtest_start_ns", "backtest_end_ns"]
        .iter()
        .all(|k| run.get(*k).map_or(false, serde_json::Value::is_string))
}

Type guard

fn is_decimal_string(v: &serde_json::Value) -> bool {
    v.as_str().map_or(false, |s| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()))
}

Try / catch

match validate_document(&doc) {
    Err(e) if e.to_string().contains("must be a decimal string") => {
        // stringify numeric run fields and retry
    }
    other => other,
}

Prevention

When it happens

Trigger: Validating a canonical result where a run field like "iterations", "total_orders", or "backtest_start_ns" is a JSON number (e.g. 42 instead of "42") or null where null is not allowed. Happens with JSON produced by a default serializer that emits raw numbers.

Common situations: Results serialized by generic JSON tools rather than the canonical writer; fixtures where the ns timestamps were written as numbers; null backtest_start_ns/backtest_end_ns passed where the field is required non-null (nullable=false for iterations/total_* fields).

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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