{"record":{"id":"e7227f22d39ad9d8","repo":"AlexsJones/llmfit","slug":"fit-to-json-returns-an-object","errorCode":null,"errorMessage":"fit_to_json returns an object","messagePattern":"fit_to_json returns an object","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"llmfit-tui/src/display.rs","lineNumber":790,"sourceCode":"        \"{}\",\n        serde_json::to_string_pretty(&output).expect(\"JSON serialization failed\")\n    );\n}\n\nfn system_json(specs: &SystemSpecs) -> serde_json::Value {\n    crate::serve_shared::system_json(specs)\n}\n\n/// CLI `fit --json` envelope: the shared serializer plus this frontend's legacy\n/// overlays. The overlaid keys carry human-readable strings the API/MCP side\n/// expresses as machine codes (with the human string under a `*_label` key);\n/// the CLI's overloaded values are load-bearing for existing scripts, so they\n/// stay put here until a future PR deprecates them (see #759).\nfn fit_to_json(fit: &ModelFit) -> serde_json::Value {\n    let mut value = crate::serve_shared::fit_to_json(fit);\n    let obj = value\n        .as_object_mut()\n        .expect(\"fit_to_json returns an object\");\n    obj.insert(\"fit_level\".to_string(), serde_json::json!(fit.fit_text()));\n    obj.insert(\n        \"run_mode\".to_string(),\n        serde_json::json!(fit.run_mode_text()),\n    );\n    obj.insert(\"runtime\".to_string(), serde_json::json!(fit.runtime_text()));\n    obj.insert(\n        \"capabilities\".to_string(),\n        serde_json::json!(\n            fit.model\n                .capabilities\n                .iter()\n                .map(|c| c.label())\n                .collect::<Vec<_>>()\n        ),\n    );\n    value\n}","sourceCodeStart":772,"sourceCodeEnd":808,"githubUrl":"https://github.com/AlexsJones/llmfit/blob/acc7e40c3a0afbd36510a92f2f8f3d5177cfc0fe/llmfit-tui/src/display.rs#L772-L808","documentation":"Panic from .expect(\"fit_to_json returns an object\") in the CLI's fit_to_json wrapper (display.rs:782-800). It takes the serde_json::Value returned by the shared serializer crate::serve_shared::fit_to_json and calls as_object_mut() so it can overlay legacy keys (fit_level, run_mode, runtime, capabilities) that scripts depend on (see the #759 comment in-source). The expect encodes a cross-module API contract: the shared serializer must return a JSON object; if it ever returns an array, string, null, or a wrapped/versioned envelope, this layer panics.","triggerScenarios":"Any `llmfit fit --json`-family command after serve_shared::fit_to_json (llmfit-tui/src/serve_shared.rs) is changed to return a non-object Value — e.g. someone wraps the payload for API versioning as {\"v2\": {...}} or returns serde_json::Value::Null for an unknown model. The related bare .unwrap() in display_json_fits_with_llamacpp (display.rs:528) panics first for the llamacpp variant.","commonSituations":"Refactors that unify the API/MCP serializer and change its return shape without updating the CLI overlay; adding a null/error sentinel to fit_to_json for models with missing data; the two frontends drifting during parallel feature work.","solutions":["Check git history / recent changes to serve_shared::fit_to_json — this expect only fires when that function stops returning an object.","Restore the contract: keep serve_shared::fit_to_json returning a JSON object for every ModelFit, or update the CLI overlay in display.rs to handle the new shape (e.g. unwrap the versioned envelope before overlaying).","Add a unit test asserting serve_shared::fit_to_json(&mock_fit).is_object() so CI catches shape drift.","If you are consuming the CLI, switch to parsing `llmfit fit --json` output only, which exercises the shared serializer's stable contract."],"exampleFix":"// before\nlet obj = value\n    .as_object_mut()\n    .expect(\"fit_to_json returns an object\");\n\n// after\nlet Some(obj) = value.as_object_mut() else {\n    eprintln!(\"internal error: fit_to_json produced non-object JSON for {}\", fit.model.name);\n    std::process::exit(1);\n};","handlingStrategy":"type-guard","validationCode":"// Before calling any display_json_fits* helper, verify the shared serializer's contract.\nlet ok = fits.iter().all(|f| crate::serve_shared::fit_to_json(f).is_object());\nif !ok {\n    eprintln!(\"shared fit_to_json violated its object contract; skipping JSON output\");\n    return;\n}\ndisplay::display_json_fits(specs, fits);","typeGuard":"fn shared_fit_json_is_object(fit: &llmfit_core::fit::ModelFit) -> bool {\n    crate::serve_shared::fit_to_json(fit).is_object()\n}","tryCatchPattern":"let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    let _ = display::fit_to_json(fit); // overlay path that asserts object shape\n}));\nif result.is_err() {\n    eprintln!(\"error: fit_to_json shape contract broken; this is a bug — please report it\");\n    std::process::exit(1);\n}","preventionTips":["Pin the contract with a test: assert serve_shared::fit_to_json(&mock_fit).is_object() for every runtime variant.","If versioning the shared serializer, unwrap the envelope inside serve_shared::fit_to_json so callers always receive the bare object.","Never return Value::Null from serve_shared::fit_to_json as an error sentinel — return Option/Result instead so the object invariant holds."],"tags":["rust","serde-json","panic","api-contract","type-mismatch"],"backgroundTag":"json-shape-mismatch","analyzedSha":"acc7e40c3a0afbd36510a92f2f8f3d5177cfc0fe","analyzedAt":"2026-08-17T10:35:29.658Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}