headroomlabs-ai/headroom · error · ConfigError

simulator config is not valid JSON: {0}

Error message

simulator config is not valid JSON: {0}

What it means

Raised by load_config when the file was read successfully but serde_json::from_str fails to parse it as a SimulatorConfig. The #[from] attribute converts any serde_json::Error (syntax error or type mismatch against the struct) into ConfigError::Parse. The embedded message pinpoints the line/column of the offending JSON.

Source

Thrown at crates/headroom-simulators/src/config.rs:63

    pub sse: Vec<SseFrame>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SseFrame {
    #[serde(default)]
    pub event: Option<String>,
    pub data: Value,
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("failed to read simulator config {path}: {source}")]
    Read {
        path: String,
        #[source]
        source: std::io::Error,
    },
    #[error("simulator config is not valid JSON: {0}")]
    Parse(#[from] serde_json::Error),
}

pub fn load_config(path: Option<&Path>) -> Result<SimulatorConfig, ConfigError> {
    let Some(path) = path else {
        return Ok(SimulatorConfig::default());
    };
    let raw = fs::read_to_string(path).map_err(|source| ConfigError::Read {
        path: path.display().to_string(),
        source,
    })?;
    Ok(serde_json::from_str(&raw)?)
}

fn default_status() -> u16 {
    200
}

View on GitHub (pinned to 322425c43b)

Solutions

  1. Validate the file with an external parser first: jq . sim.json — any syntax error is reported with a position.
  2. Read the serde error message: 'expected struct SimulatorConfig' means a field type mismatch; fix the field named in the message.
  3. Cross-check against the struct definition in crates/headroom-simulators/src/config.rs — note sse entries require a data field (event is optional via #[serde(default)]).
  4. If you upgraded the crate recently, re-copy the example config from the new version and re-apply your changes.

Example fix

// before
{"sse": {"event": "x", "data": {}}}

// after
{"sse": [{"event": "x", "data": {"role": "assistant"}}]}
Defensive patterns

Strategy: validation

Validate before calling

fn json_parses(raw: &str) -> Result<(), String> {
    serde_json::from_str::<serde_json::Value>(raw)
        .map(|_| ())
        .map_err(|e| e.to_string())
}

// and schema check: the "sse" array entries need a "data" field
let v: serde_json::Value = serde_json::from_str(&raw)?;
assert!(v.get("sse").map_or(true, |s| s.as_array().is_some()));

Try / catch

match serde_json::from_str::<SimulatorConfig>(&raw) {
    Err(e) if e.classify() == serde_json::error::Category::Syntax => {
        eprintln!("config is not valid JSON at line {}: fix syntax", e.line());
    }
    Err(e) => eprintln!("config field mismatch: {e}"),
    Ok(cfg) => { /* proceed */ }
}

Prevention

When it happens

Trigger: Passing a config file containing invalid JSON (trailing commas, comments, unquoted keys) or valid JSON that does not match SimulatorConfig's shape (e.g. 'sse' is not an array of {event?, data} objects).

Common situations: Hand-edited config with a trailing comma or JSONC comment, a YAML config fed where JSON is expected, or a schema drift after upgrading headroom-simulators (renamed/removed fields without #[serde(default)]).

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/a4d4409df38017a0. Report an issue: GitHub.