{"record":{"id":"3d79518e018c2aa6","repo":"BigPizzaV3/CodexPlusPlus","slug":"latest-json-missing-version","errorCode":null,"errorMessage":"latest.json missing version","messagePattern":"latest\\.json missing version","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/update.rs","lineNumber":114,"sourceCode":"            .and_then(Value::as_str)\n            .unwrap_or_default()\n            .to_string(),\n        body: payload\n            .get(\"body\")\n            .and_then(Value::as_str)\n            .unwrap_or_default()\n            .to_string(),\n        asset_name: selected.as_ref().map(|asset| asset.name.clone()),\n        asset_url: selected.map(|asset| asset.browser_download_url),\n    })\n}\n\npub fn release_from_latest_json_payload(payload: &Value) -> anyhow::Result<Release> {\n    let version = payload\n        .get(\"version\")\n        .or_else(|| payload.get(\"tag_name\"))\n        .and_then(Value::as_str)\n        .ok_or_else(|| anyhow::anyhow!(\"latest.json missing version\"))?\n        .to_string();\n    let assets = payload\n        .get(\"assets\")\n        .and_then(Value::as_array)\n        .into_iter()\n        .flatten()\n        .filter_map(|asset| {\n            let name = asset.get(\"name\")?.as_str()?.to_string();\n            let url = asset\n                .get(\"url\")\n                .or_else(|| asset.get(\"browser_download_url\"))?\n                .as_str()?\n                .to_string();\n            Some((name, url))\n        })\n        .collect::<Vec<_>>();\n    let selected = select_update_asset(&assets);\n    Ok(Release {","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/update.rs#L96-L132","documentation":"release_from_latest_json_payload parses the latest.json manifest published alongside releases. It looks up the version from 'version' first, falling back to 'tag_name'; both must be strings. If neither field exists (or neither is a string), the release version is unknown and the parser throws this error rather than producing a Release with a bogus version.","triggerScenarios":"Calling release_from_latest_json_payload (directly or via fetch_latest_release) with: (1) an empty JSON object, (2) a latest.json generated by a build pipeline that renamed/dropped the 'version' key, (3) an error page or CDN stub returned as JSON, (4) a payload where version/tag_name are numbers rather than strings.","commonSituations":"A maintainer regenerates latest.json with a new tool that uses a different schema key; the release asset URL points at a 404 HTML/JSON error body; versioning tooling (e.g. cargo-release configs) changed the manifest format; a stale cached manifest from an older schema.","solutions":["Open the latest.json being fetched and confirm it has a top-level string 'version' (or 'tag_name') field.","Regenerate/upload the latest.json asset with the required schema: {\"version\":\"x.y.z\",\"assets\":[{\"name\":...,\"url\":...}]}.","Verify the URL (DEFAULT_LATEST_JSON_URL) actually serves the manifest, not an error body — check HTTP status before parsing.","If the field is a number (e.g. version: 1.2), fix the generator to emit it as a string.","For local parsing of third-party manifests, normalize first: copy the version into 'version' before calling the parser."],"exampleFix":"// before\nlet payload: serde_json::Value = resp.json().await?;\nlet release = release_from_latest_json_payload(&payload)?;\n// after\nlet payload: serde_json::Value = resp.json().await?;\nlet payload = match payload.get(\"version\").or_else(|| payload.get(\"tag_name\")) {\n    Some(v) if v.as_str().is_some() => payload,\n    _ => return Err(anyhow::anyhow!(\"latest.json has no version/tag_name: {}\", payload)),\n};\nlet release = release_from_latest_json_payload(&payload)?;","handlingStrategy":"validation","validationCode":"fn latest_json_has_version(payload: &serde_json::Value) -> bool {\n    [\"version\", \"tag_name\"].iter().any(|k| {\n        payload.get(k).and_then(|v| v.as_str()).map_or(false, |s| !s.is_empty())\n    })\n}","typeGuard":"fn manifest_version<'a>(payload: &'a serde_json::Value) -> Option<&'a str> {\n    payload.get(\"version\").or_else(|| payload.get(\"tag_name\"))?.as_str()\n}","tryCatchPattern":"match release_from_latest_json_payload(&payload) {\n    Ok(release) => release,\n    Err(e) if e.to_string().contains(\"latest.json missing version\") => {\n        eprintln!(\"manifest schema mismatch: {}\", serde_json::to_string(&payload).unwrap_or_default());\n        return Err(e);\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Pin the latest.json schema ({version, assets:[{name,url}]}) in CI so regeneration can't drop 'version'.","Check HTTP status is 200 before parsing the fetched manifest.","Always emit version as a JSON string, never a number.","Detect empty/error bodies (HTML 404 pages parsed as JSON objects) before calling the parser."],"tags":["latest-json","release-parsing","json-validation","update-check"],"backgroundTag":"missing-required-argument","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"}