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
- Inspect the named file — the message embeds the full path — and confirm its top-level type (jq type file.json)
- Restore a known-good object-shaped file (e.g. '{}') or the snapshot counterpart if available
- Delete the offending state file so it is treated as absent (path.exists() returns Ok(None)) and gets recreated with the correct shape
- 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
- Only write objects as the state root; never arrays or scalars
- Back up state files before external tools touch them
- Snapshot state (the codebase keeps a snapshot counterpart) so recovery is possible
- Validate external input before persisting it to the state path
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
- {table_name} 必须是 TOML 表
- auth.json 必须是 JSON 对象
- model_providers.{provider_id} 必须是 TOML table
- release payload missing tag_name
- latest.json missing version
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16).
Data as JSON: /api/errors/2299042bacaffefe.
Report an issue: GitHub.