{"record":{"id":"4b8f1e9fda378298","repo":"BigPizzaV3/CodexPlusPlus","slug":"field-name-json","errorCode":null,"errorMessage":"{field_name} 必须是 JSON 对象","messagePattern":"(.+?) 必须是 JSON 对象","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/relay_config.rs","lineNumber":2106,"sourceCode":"    let catalog_json = apply_model_metadata_overrides(&catalog_json, &model_metadata)?;\n    crate::settings::atomic_write(&catalog_path, catalog_json.as_bytes())?;\n    let mut doc = parse_toml_document(&config_text)?;\n    doc[\"model_catalog_json\"] = toml_edit::value(catalog_relative);\n    Ok(normalize_optional_toml(doc))\n}\n\nfn parse_model_string_map(\n    value: &str,\n    field_name: &str,\n) -> anyhow::Result<HashMap<String, String>> {\n    if value.trim().is_empty() {\n        return Ok(HashMap::new());\n    }\n    let parsed: Value = serde_json::from_str(value)\n        .map_err(|error| anyhow::anyhow!(\"{field_name} JSON 解析失败：{error}\"))?;\n    let object = parsed\n        .as_object()\n        .ok_or_else(|| anyhow::anyhow!(\"{field_name} 必须是 JSON 对象\"))?;\n    object\n        .iter()\n        .map(|(key, value)| {\n            let value = value\n                .as_str()\n                .ok_or_else(|| anyhow::anyhow!(\"{field_name} 的模型 {key} 值必须是字符串\"))?;\n            Ok((key.clone(), value.to_string()))\n        })\n        .collect()\n}\n\nfn validate_model_windows(model_windows: &HashMap<String, String>) -> anyhow::Result<()> {\n    for (slug, value) in model_windows {\n        if crate::model_suffix::parse_window_token(value).is_none() {\n            anyhow::bail!(\"model_windows 的模型 {slug} 窗口值无效：{value}\");\n        }\n    }\n    Ok(())","sourceCodeStart":2088,"sourceCodeEnd":2124,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/relay_config.rs#L2088-L2124","documentation":"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.","triggerScenarios":"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\"`.","commonSituations":"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.","solutions":["Wrap the value in a JSON object: {\"<model-slug>\": \"<value>\"} — top level must be `{...}`.","If you have a list of models, convert each to a key with a value rather than an array.","Validate with code: parse then check it is an object before assigning to the profile field.","Clear the field if no mapping is needed (empty string is valid)."],"exampleFix":"// before\nmodel_windows = \"[\\\"deepseek-v4-pro\\\"]\"\n// after\nmodel_windows = \"{\\\"deepseek-v4-pro\\\": \\\"1000000\\\"}\"","handlingStrategy":"type-guard","validationCode":"fn is_json_object(json: &str) -> bool {\n    serde_json::from_str::<serde_json::Value>(json)\n        .map(|v| v.is_object())\n        .unwrap_or(false)\n}","typeGuard":"fn is_object(v: &serde_json::Value) -> bool { v.is_object() }","tryCatchPattern":"match normalize_relay_profile_for_storage(profile) {\n    Err(e) if e.to_string().contains(\"必须是 JSON 对象\") => {\n        // coerce/wrap the value into {\"key\": value} or reject in the UI\n    }\n    other => other?,\n}","preventionTips":["Ensure the top level of map fields is always {\"...\": ...}","Convert lists to keyed objects before assigning to profile fields","Validate with is_object() in form validation before save"],"tags":["json","schema","config"],"backgroundTag":"unexpected-response-shape","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}