Hmbown/CodeWhale · error

serialize

Error message

serialize

What it means

This is the panic from `serde_json::to_string(&snapshot).expect("serialize")` in the `full_snapshot_round_trips` test at crates/protocol/src/agent_run.rs:254. `AgentRunSnapshot` is a dependency-neutral, JSON-serializable read model of an agent run, and the test asserts a fully-populated snapshot (all budget fields set, terminal summary, one receipt ref) can be serialized to JSON. The library/test effectively throws this when serde_json cannot serialize the value — e.g. a `Serialize` implementation that emits a non-string map key, a poison/malformed value, or a type that lost its `#[derive(Serialize)]`. For an all-owned plain-data type like `AgentRunSnapshot` this almost never fires at runtime; it fires only if the struct or one of its field types (enums, `BudgetSummary`, `TerminalSummary`, `ReceiptRef`) was changed in a way that breaks serialization (e.g. serde attributes like `skip_serializing_if` pointing at a missing/renamed method, or a field type replaced with a non-serializable one).

Solutions

  1. Check the panic's underlying serde error (the `expect` message is terse; temporarily use `.unwrap_or_else(|e| panic!("serialize: {e}"))` or run with RUST_BACKTRACE and inspect the serde_json error) to see which field fails to serialize.
  2. Verify every type reachable from `AgentRunSnapshot` still derives or implements `Serialize` — including new fields you just added — and that nested values are JSON-compatible (string map keys, no handles or callbacks; the module doc at the top of agent_run.rs requires scalar/serialized data only).
  3. Audit serde attributes on changed fields: `#[serde(default, skip_serializing_if = "Option::is_none")]` must only be on `Option` fields, `skip_serializing_if = "Vec::is_empty"` only on `Vec` fields, and the referenced function must exist.
  4. If a newly added field cannot serialize to JSON (non-string map keys, unserializable datetime), map it to a serialized/scalar form first (e.g. store `String` IDs and epoch-ms integers, as the existing fields do) or add a serde `with` helper.
  5. Re-run the specific test to confirm: `cargo test -p codewhale-protocol full_snapshot_round_trips`.

Example fix

// before
pub struct AgentRunSnapshot {
    pub run_id: String,
    pub owners: std::collections::HashMap<RunId, String>, // non-string keys, RunId not Serialize
}
// after
pub struct AgentRunSnapshot {
    pub run_id: String,
    /// Serialized IDs only — JSON-compatible map keys.
    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
    pub owners: std::collections::HashMap<String, String>,
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Compile-time guard that the whole snapshot type is JSON-serializable
fn assert_json_roundtrip<T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(v: &T) {
    let json = serde_json::to_string(v).expect("serialize");
    let back: T = serde_json::from_str(&json).expect("deserialize");
    assert_eq!(&back, v);
}
assert_json_roundtrip(&snapshot);

Type guard

fn serializable<T: serde::Serialize>(v: &T) -> Option<String> {
    serde_json::to_string(v).ok()
}

Try / catch

let json = serde_json::to_string(&snapshot)
    .map_err(|e| format!("snapshot serialize failed: {e}"))?; // keep the serde error, don't expect() it away

Prevention

When it happens

Trigger: Calling `serde_json::to_string` (or `to_value`/`to_writer`) on an `AgentRunSnapshot` whose type or field types no longer implement `Serialize` correctly: (1) removing `#[derive(Serialize)]` from `AgentRunSnapshot`, `BudgetSummary`, `TerminalSummary`, `ReceiptRef`, or one of the enums (`RunSource`, `RunState`, `TerminalOutcome`, `ReceiptKind`); (2) adding a field whose type fails to serialize (e.g. a map with non-string keys or an untagged enum that errors); (3) a broken serde attribute such as `skip_serializing_if = "Option::is_none"` misspelled or attached to a non-Option field; (4) serializing with a serializer that rejects a value shape the type emits (e.g. a map key that is not a string).

Common situations: A developer edits `AgentRunSnapshot` in crates/protocol/src/agent_run.rs to add a richer field (a new handle, a HashMap keyed by a struct, a datetime without a serde helper) and runs `cargo test -p codewhale-protocol`; the round-trip test panics with the bare message "serialize" at line 254. Also seen when a refactor swaps an enum for a struct-with-generic or introduces `#[serde(untagged)]` combos that produce serializer errors, or when a serde helper referenced in an attribute no longer resolves after a rename.

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/8f335efc6a999688. Report an issue: GitHub.

Appendix: source

Thrown at crates/protocol/src/agent_run.rs:254

        }
    }

    fn minimal_snapshot() -> AgentRunSnapshot {
        AgentRunSnapshot {
            run_id: "job-42".to_string(),
            parent: None,
            source: RunSource::CoreJob,
            state: RunState::Queued,
            budget: BudgetSummary::default(),
            terminal: None,
            refs: Vec::new(),
        }
    }

    #[test]
    fn full_snapshot_round_trips() {
        let snapshot = full_snapshot();
        let json = serde_json::to_string(&snapshot).expect("serialize");
        let back: AgentRunSnapshot = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, snapshot);
        assert!(back.is_coherent());
    }

    #[test]
    fn minimal_snapshot_round_trips_and_skips_empty_fields() {
        let snapshot = minimal_snapshot();
        let json = serde_json::to_string(&snapshot).expect("serialize");
        // Optional/empty fields stay off the wire entirely.
        assert!(!json.contains("parent"));
        assert!(!json.contains("terminal"));
        assert!(!json.contains("refs"));
        assert!(!json.contains("token_budget"));
        let back: AgentRunSnapshot = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, snapshot);
        assert!(back.is_coherent());
    }

View on GitHub (pinned to 433685b202)