nautechsystems/nautilus_trader · error

Expected 2 outcomes, received {}

Error message

Expected 2 outcomes, received {}

What it means

parse_gamma_market requires the market's outcomes JSON array to contain exactly two outcomes, matching the two CLOB token IDs of a binary market. Any other count aborts the parse because instrument construction assumes a YES/NO pair.

Source

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

            .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
        .unwrap_or(DEFAULT_TICK_SIZE);
    let price_precision = POLYMARKET_PRICE_PRECISION;

    // Polymarket charges fees using `feeSchedule.rate` on the Gamma market.
    // Only takers pay; makers are always zero.
    // Reference: https://docs.polymarket.com/trading/fees
    let maker_fee: Option<Decimal> = market.fee_schedule.as_ref().map(|_| Decimal::ZERO);
    let taker_fee: Option<Decimal> = market.fee_schedule.as_ref().map(|fs| fs.rate);

    let min_size = market.order_min_size;

    let active = market.active.unwrap_or(false)
        && !market.closed.unwrap_or(false)
        && market.accepting_orders.unwrap_or(false);

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Pre-filter markets to those with exactly two outcomes before parsing into instruments.
  2. Skip and log the offending market.condition_id so one bad market does not fail the whole batch.
  3. Verify the raw outcomes JSON from the Gamma API to detect schema changes.
  4. Handle multi-outcome events with dedicated parsing logic if support is needed.

Example fix

// before
if outcomes.len() != 2 {
    anyhow::bail!("Expected 2 outcomes, received {}", outcomes.len());
}
// after (caller-side guard)
let outcomes: Vec<String> = serde_json::from_str(&market.outcomes)?;
if outcomes.len() != 2 { log::warn!("skipping non-binary market {}", market.condition_id); continue; }
Defensive patterns

Strategy: validation

Validate before calling

fn has_two_outcomes(m: &GammaMarket) -> bool {
    serde_json::from_str::<Vec<String>>(&m.outcomes)
        .map(|o| o.len() == 2)
        .unwrap_or(false)
}
// filter: markets.retain(has_two_outcomes);

Type guard

fn is_binary_outcomes(raw: &str) -> bool {
    matches!(serde_json::from_str::<Vec<String>>(raw), Ok(v) if v.len() == 2)
}

Try / catch

let outcomes: Vec<String> = serde_json::from_str(&market.outcomes)?;
if outcomes.len() != 2 {
    log::warn!("skipping market {} with {} outcomes", market.condition_id, outcomes.len());
    return Ok(None); // skip instead of failing batch
}

Prevention

When it happens

Trigger: A Gamma market whose outcomes field deserializes to fewer or more than 2 strings (multi-outcome markets, empty outcomes arrays, malformed outcomes JSON).

Common situations: Fetching sports or neg-risk multi-outcome events where a market can list several outcomes; API responses for very new markets with empty outcome arrays; Gamma API payload drift.

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