BigPizzaV3/CodexPlusPlus · error · anyhow::Error

{} must be a JSON object

Error message

{} must be a JSON object

What it means

load_state (crates/codex-plus-core/src/codex_app_state.rs:182) reads the persisted codex-app state file, parses it as JSON successfully, and then requires the top-level value to be an object; anything else (array, string, number, bool, null) yields '<path> must be a JSON object'. Because parsing already succeeded, this is a shape/type error, not a syntax error — the file is valid JSON of the wrong structure.

Source

Thrown at crates/codex-plus-core/src/codex_app_state.rs:182

            );
        }
    }
}

fn load_global_state(home: &Path) -> anyhow::Result<Option<Map<String, Value>>> {
    let path = state_path(home);
    if !path.exists() {
        return Ok(None);
    }
    let text =
        fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
    let value: Value = serde_json::from_str(&text)
        .with_context(|| format!("failed to parse {}", path.display()))?;
    value
        .as_object()
        .cloned()
        .map(Some)
        .ok_or_else(|| anyhow::anyhow!("{} must be a JSON object", path.display()))
}

fn load_snapshot(home: &Path) -> anyhow::Result<Option<Map<String, Value>>> {
    let path = snapshot_path(home);
    if !path.exists() {
        return Ok(None);
    }
    let text =
        fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
    let value: Value = serde_json::from_str(&text)
        .with_context(|| format!("failed to parse {}", path.display()))?;
    let state = value
        .get("state")
        .and_then(Value::as_object)
        .or_else(|| value.as_object())
        .cloned()
        .unwrap_or_default();
    Ok(Some(state))

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. Inspect the named file — the message embeds the full path — and confirm its top-level type (jq type file.json)
  2. Restore a known-good object-shaped file (e.g. '{}') or the snapshot counterpart if available
  3. Delete the offending state file so it is treated as absent (path.exists() returns Ok(None)) and gets recreated with the correct shape
  4. Find and fix whatever wrote the wrong shape (external script, older/newer version) so it does not recur

Example fix

# before: state file is a top-level array
$ cat ~/.codex-plus/state.json
[{"theme":"dark"}]

# after: top-level JSON object
$ echo '{}' > ~/.codex-plus/state.json
Defensive patterns

Strategy: validation

Validate before calling

// Pre-validate shape before relying on load_state
let text = std::fs::read_to_string(&path)?;
let v: serde_json::Value = serde_json::from_str(&text)?;
ensure!(v.is_object(), "{} must hold a JSON object", path.display());
// only now hand the path to the library

Type guard

fn is_json_object(v: &serde_json::Value) -> bool { v.is_object() }

Try / catch

match load_state(home) {
    Ok(state) => Ok(state),
    Err(e) if e.to_string().contains("must be a JSON object") => {
        // shape is wrong but file is absent-safe to reset: back it up and start fresh
        let _ = std::fs::rename(&path, path.with_extension("json.bak"));
        Ok(None) // treated as no state
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Loading a home directory whose state JSON was written by a different tool/version with a non-object root (e.g. a top-level array or a bare string), or hand-edited into that shape; the parallel load_snapshot() applies the same rule to the snapshot file.

Common situations: Manual edits to the state file; a migration or external script writing scalar/array JSON to the same path; state file shared/synced across machines with mismatched tool versions.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16). Data as JSON: /api/errors/2299042bacaffefe. Report an issue: GitHub.