AlexsJones/llmfit · error

fit_to_json returns an object

Error message

fit_to_json returns an object

What it means

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.

Source

Thrown at llmfit-tui/src/display.rs:790

        "{}",
        serde_json::to_string_pretty(&output).expect("JSON serialization failed")
    );
}

fn system_json(specs: &SystemSpecs) -> serde_json::Value {
    crate::serve_shared::system_json(specs)
}

/// CLI `fit --json` envelope: the shared serializer plus this frontend's legacy
/// overlays. The overlaid keys carry human-readable strings the API/MCP side
/// expresses as machine codes (with the human string under a `*_label` key);
/// the CLI's overloaded values are load-bearing for existing scripts, so they
/// stay put here until a future PR deprecates them (see #759).
fn fit_to_json(fit: &ModelFit) -> serde_json::Value {
    let mut value = crate::serve_shared::fit_to_json(fit);
    let obj = value
        .as_object_mut()
        .expect("fit_to_json returns an object");
    obj.insert("fit_level".to_string(), serde_json::json!(fit.fit_text()));
    obj.insert(
        "run_mode".to_string(),
        serde_json::json!(fit.run_mode_text()),
    );
    obj.insert("runtime".to_string(), serde_json::json!(fit.runtime_text()));
    obj.insert(
        "capabilities".to_string(),
        serde_json::json!(
            fit.model
                .capabilities
                .iter()
                .map(|c| c.label())
                .collect::<Vec<_>>()
        ),
    );
    value
}

View on GitHub (pinned to acc7e40c3a)

Solutions

  1. Check git history / recent changes to serve_shared::fit_to_json — this expect only fires when that function stops returning an object.
  2. 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).
  3. Add a unit test asserting serve_shared::fit_to_json(&mock_fit).is_object() so CI catches shape drift.
  4. If you are consuming the CLI, switch to parsing `llmfit fit --json` output only, which exercises the shared serializer's stable contract.

Example fix

// before
let obj = value
    .as_object_mut()
    .expect("fit_to_json returns an object");

// after
let Some(obj) = value.as_object_mut() else {
    eprintln!("internal error: fit_to_json produced non-object JSON for {}", fit.model.name);
    std::process::exit(1);
};
Defensive patterns

Strategy: type-guard

Validate before calling

// Before calling any display_json_fits* helper, verify the shared serializer's contract.
let ok = fits.iter().all(|f| crate::serve_shared::fit_to_json(f).is_object());
if !ok {
    eprintln!("shared fit_to_json violated its object contract; skipping JSON output");
    return;
}
display::display_json_fits(specs, fits);

Type guard

fn shared_fit_json_is_object(fit: &llmfit_core::fit::ModelFit) -> bool {
    crate::serve_shared::fit_to_json(fit).is_object()
}

Try / catch

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    let _ = display::fit_to_json(fit); // overlay path that asserts object shape
}));
if result.is_err() {
    eprintln!("error: fit_to_json shape contract broken; this is a bug — please report it");
    std::process::exit(1);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of AlexsJones/llmfit@acc7e40c3a (2026-08-17). Data as JSON: /api/errors/e7227f22d39ad9d8. Report an issue: GitHub.