nautechsystems/nautilus_trader · error · anyhow::Error

Expected JSON object for contract

Error message

Expected JSON object for contract

What it means

`parse_contract_from_json` expects a `serde_json::Value` that is a JSON object with contract fields (symbol, exchange, sec_type, etc.). This error is thrown when the passed value is not an object (e.g. a string, number, array, or null), so contract deserialization cannot proceed.

Source

Thrown at crates/adapters/interactive_brokers/src/common/contracts.rs:85

fn security_type_to_code(security_type: &SecurityType) -> String {
    IbSecurityType::try_from(security_type).map_or_else(
        |_| security_type.to_string(),
        |security_type| security_type.to_string(),
    )
}

/// Parse IB contract from JSON dictionary.
///
/// This function parses a JSON object (dictionary) representing an IBContract
/// and converts it to a rust-ibapi Contract struct.
///
/// # Errors
///
/// Returns an error if the JSON is not a valid object or if required fields are missing.
pub fn parse_contract_from_json(json: &Value) -> anyhow::Result<Contract> {
    let obj = json
        .as_object()
        .ok_or_else(|| anyhow::anyhow!("Expected JSON object for contract"))?;

    let get_str = |key: &str| -> String {
        obj.get(key)
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string()
    };

    let get_i32 = |key: &str| -> i32 {
        obj.get(key)
            .and_then(|v| v.as_i64())
            .map_or(0, |n| n as i32)
    };

    let get_f64 = |key: &str| -> f64 { obj.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0) };

    let get_bool = |key: &str| -> bool { obj.get(key).and_then(|v| v.as_bool()).unwrap_or(false) };

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect the JSON value before parsing; ensure each contract entry is a JSON object like `{"symbol": "AAPL", ...}`.
  2. Fix double-encoded JSON: if the value is a string containing JSON, call `serde_json::from_str::<Value>(s)` first to unwrap it.
  3. Filter out null/non-object entries before iterating an array of contracts.
  4. Regenerate the contract cache file if it was hand-edited or written by an older adapter version.

Example fix

// before
let contract = parse_contract_from_json(&value)?;
// after
if !value.is_object() {
    return Err(anyhow::anyhow!("skipping non-object contract entry"));
}
let contract = parse_contract_from_json(&value)?;
Defensive patterns

Strategy: type-guard

Validate before calling

if !json.is_object() {
    return Err(anyhow::anyhow!("contract entry must be a JSON object"));
}

Type guard

fn is_contract_object(v: &serde_json::Value) -> bool {
    v.is_object() && v.get("symbol").map_or(false, |s| s.is_string())
}

Try / catch

match parse_contract_from_json(&value) {
    Ok(c) => contracts.push(c),
    Err(e) if e.to_string().contains("Expected JSON object") => {
        log::warn!("skipping non-object contract entry: {value}");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `parse_contract_from_json` directly with a non-object `Value`, or via `parse_contracts_from_json_array`, `load_contract`, `contract_from_instrument_info`, `load_cache`, or `py_to_contract` when the underlying JSON element is a scalar/string/array rather than an object.

Common situations: A cache file or IPC/Python boundary produced contract entries as JSON strings (double-encoded) instead of objects; a contracts array contains `null` or numeric placeholders; hand-edited cache JSON is malformed.

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/2c5f61a967df21b1. Report an issue: GitHub.