nautechsystems/nautilus_trader · error

Failed to parse clob_token_ids '{}': {e}

Error message

Failed to parse clob_token_ids '{}': {e}

What it means

parse_gamma_market expects the market's clob_token_ids field to be a JSON string containing a JSON array (Polymarket returns it as a stringified array). serde_json::from_str fails when the string is not valid JSON or is not an array of strings, producing this error.

Source

Thrown at crates/adapters/polymarket/src/http/parse.rs:111

    /// numeric and composite `<uuid>:<away>:<home>` forms.
    pub game_id: Option<String>,
}

/// Parses a Gamma market response into instrument definitions.
///
/// Each market produces two definitions: one for the Yes outcome
/// and one for the No outcome.
pub fn parse_gamma_market(market: &GammaMarket) -> anyhow::Result<Vec<PolymarketInstrumentDef>> {
    let game_id = market.game_id.clone().or_else(|| {
        market
            .events
            .as_ref()?
            .iter()
            .find_map(|event| event.game_id.clone())
    });

    let token_ids: Vec<String> = serde_json::from_str(&market.clob_token_ids).map_err(|e| {
        anyhow::anyhow!(
            "Failed to parse clob_token_ids '{}': {e}",
            market.clob_token_ids
        )
    })?;

    if token_ids.len() != 2 {
        anyhow::bail!("Expected 2 token IDs, received {}", token_ids.len());
    }

    let outcomes: Vec<String> = serde_json::from_str(&market.outcomes)
        .map_err(|e| anyhow::anyhow!("Failed to parse outcomes '{}': {e}", market.outcomes))?;

    if outcomes.len() != 2 {
        anyhow::bail!("Expected 2 outcomes, received {}", outcomes.len());
    }

    let tick_size = market
        .order_price_min_tick_size

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Log the raw market.clob_token_ids value to see the actual payload shape.
  2. If the field arrives as a real array (not a string), parse it with serde_json::from_value or update the GammaMarket struct to use an untagged/ custom deserializer.
  3. Report/handle the Gamma schema change and pin to a known-good API behavior; skip markets with unparseable token ids.

Example fix

// before
let token_ids: Vec<String> = serde_json::from_str(&market.clob_token_ids)...;
// after
let token_ids: Vec<String> = match serde_json::from_str(&market.clob_token_ids) {
    Ok(v) => v,
    Err(_) => serde_json::from_value(market.clob_token_ids_raw.clone())?, // handles non-string shapes
};
Defensive patterns

Strategy: validation

Validate before calling

fn has_stringified_token_ids(market: &GammaMarket) -> bool {
    !market.clob_token_ids.is_empty()
        && market.clob_token_ids.starts_with('[')
        && serde_json::from_str::<Vec<String>>(&market.clob_token_ids).is_ok()
}

Type guard

fn parse_token_ids(raw: &str) -> Option<Vec<String>> {
    serde_json::from_str::<Vec<String>>(raw).ok()
}

Try / catch

let token_ids = match serde_json::from_str::<Vec<String>>(&market.clob_token_ids) {
    Ok(v) => v,
    Err(e) => { warn!("skipping market {}: bad clob_token_ids: {e}", market.id); return Ok(None); }
};

Prevention

When it happens

Trigger: Gamma API returning clob_token_ids as an actual JSON array instead of a string, an empty string, or malformed/quoted-inconsistently content while parsing a market into instruments.

Common situations: Gamma API schema changes, proxy/mock servers re-serializing the field differently, or truncated/empty responses from the events endpoint.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


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