BigPizzaV3/CodexPlusPlus · error

必须是 JSON 对象

Error message

{field_name} 必须是 JSON 对象

What it means

After parse_model_string_map successfully parses the JSON, it requires the top-level value to be a JSON object (serde_json::Value::as_object). If the field parses to an array, string, number, or null, it throws "{field_name} 必须是 JSON 对象". The map format is mandatory because entries are read as key→string pairs.

Solutions

  1. Wrap the value in a JSON object: {"<model-slug>": "<value>"} — top level must be `{...}`.
  2. If you have a list of models, convert each to a key with a value rather than an array.
  3. Validate with code: parse then check it is an object before assigning to the profile field.
  4. Clear the field if no mapping is needed (empty string is valid).

Example fix

// before
model_windows = "[\"deepseek-v4-pro\"]"
// after
model_windows = "{\"deepseek-v4-pro\": \"1000000\"}"
Defensive patterns

Strategy: type-guard

Validate before calling

fn is_json_object(json: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(json)
        .map(|v| v.is_object())
        .unwrap_or(false)
}

Type guard

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

Try / catch

match normalize_relay_profile_for_storage(profile) {
    Err(e) if e.to_string().contains("必须是 JSON 对象") => {
        // coerce/wrap the value into {"key": value} or reject in the UI
    }
    other => other?,
}

Prevention

When it happens

Trigger: Setting a profile JSON-map field (model_windows etc.) to valid JSON that is not an object, e.g. `"[\"deepseek-v4-pro\"]"`, `"\"1M\""`, or `"1000000"`.

Common situations: Users pasting a JSON array of model names, a bare number as the window size, or quoted scalar values instead of a slug→value mapping.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/4b8f1e9fda378298. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/relay_config.rs:2106

    let catalog_json = apply_model_metadata_overrides(&catalog_json, &model_metadata)?;
    crate::settings::atomic_write(&catalog_path, catalog_json.as_bytes())?;
    let mut doc = parse_toml_document(&config_text)?;
    doc["model_catalog_json"] = toml_edit::value(catalog_relative);
    Ok(normalize_optional_toml(doc))
}

fn parse_model_string_map(
    value: &str,
    field_name: &str,
) -> anyhow::Result<HashMap<String, String>> {
    if value.trim().is_empty() {
        return Ok(HashMap::new());
    }
    let parsed: Value = serde_json::from_str(value)
        .map_err(|error| anyhow::anyhow!("{field_name} JSON 解析失败:{error}"))?;
    let object = parsed
        .as_object()
        .ok_or_else(|| anyhow::anyhow!("{field_name} 必须是 JSON 对象"))?;
    object
        .iter()
        .map(|(key, value)| {
            let value = value
                .as_str()
                .ok_or_else(|| anyhow::anyhow!("{field_name} 的模型 {key} 值必须是字符串"))?;
            Ok((key.clone(), value.to_string()))
        })
        .collect()
}

fn validate_model_windows(model_windows: &HashMap<String, String>) -> anyhow::Result<()> {
    for (slug, value) in model_windows {
        if crate::model_suffix::parse_window_token(value).is_none() {
            anyhow::bail!("model_windows 的模型 {slug} 窗口值无效:{value}");
        }
    }
    Ok(())

View on GitHub (pinned to b1ed92e5e4)