{"record":{"id":"977716ae625be3e0","repo":"Hmbown/CodeWhale","slug":"credential-file-must-be-a-json-object-of-entries","errorCode":null,"errorMessage":"credential file {} must be a JSON object of entries","messagePattern":"credential file (.+?) must be a JSON object of entries","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/oauth.rs","lineNumber":1725,"sourceCode":"fn load_owned_auth_file_from_store(\n    store: &codewhale_config::XaiOAuthCredentialStore,\n    name: &str,\n) -> Result<Option<AuthFile>> {\n    let Some(raw) = store.read_to_string(name)? else {\n        return Ok(None);\n    };\n    parse_auth_file(&raw, &store.path_for(name)?).map(Some)\n}\n\nfn parse_auth_file(raw: &str, path: &Path) -> Result<AuthFile> {\n    let value: Value = serde_json::from_str(raw).map_err(|_| {\n        anyhow::anyhow!(\n            \"credential file {} is not valid credential JSON\",\n            codewhale_config::quote_os_path(path)\n        )\n    })?;\n    let obj = value.as_object().ok_or_else(|| {\n        anyhow::anyhow!(\n            \"credential file {} must be a JSON object of entries\",\n            codewhale_config::quote_os_path(path)\n        )\n    })?;\n    let mut out = BTreeMap::new();\n    for (k, v) in obj {\n        match serde_json::from_value::<OwnedAuthEntry>(v.clone()) {\n            Ok(entry) => {\n                out.insert(k.clone(), entry);\n            }\n            Err(_) => {\n                tracing::warn!(\n                    target: \"codewhale::oauth\",\n                    \"skipping unreadable owned auth entry\"\n                );\n            }\n        }\n    }","sourceCodeStart":1707,"sourceCodeEnd":1743,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L1707-L1743","documentation":"After JSON parsing succeeds, parse_auth_file requires the top-level value to be a JSON object whose keys are credential entry names. Arrays, strings, numbers, or null at the top level produce this error. The schema is a map of named entries, not a bare credential.","triggerScenarios":"parse_auth_file receives valid JSON whose root is not an object — e.g. a file containing a single credential object instead of {\"name\": {...}}, or an array of entries.","commonSituations":"User pastes one provider's credential JSON (from another tool's export) directly into the store instead of nesting it under an entry name; a migration wrote the wrong shape.","solutions":["Wrap the credential in a named entry: {\"<entry-name>\": { ...credential... }}.","Delete the file and re-run the provider login to regenerate the correct shape.","If migrating from another tool, use the documented import path (e.g. `codewhale auth xai-device` or the Grok CLI import) instead of hand-copying."],"exampleFix":"// before\n{ \"access_token\": \"...\", \"refresh_token\": \"...\" }\n// after\n{ \"default\": { \"access_token\": \"...\", \"refresh_token\": \"...\" } }","handlingStrategy":"validation","validationCode":"const v = JSON.parse(fs.readFileSync(p, 'utf8')); if (v === null || typeof v !== 'object' || Array.isArray(v)) restructure(p);","typeGuard":"const isEntryMap = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);","tryCatchPattern":"try { useCredentials(); } catch (e) { if (String(e).includes('must be a JSON object of entries')) { await relogin(); } }","preventionTips":["Wrap imported credentials under a named entry key","Use documented import commands instead of copying raw credential JSON","Keep the top-level shape {\"entry\": {credential}} in any migration script"],"tags":["oauth","json","schema"],"backgroundTag":"schema-validation-failed","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}