{"record":{"id":"8f335efc6a999688","repo":"Hmbown/CodeWhale","slug":"serialize-agent-run","errorCode":null,"errorMessage":"serialize","messagePattern":"serialize","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/protocol/src/agent_run.rs","lineNumber":254,"sourceCode":"        }\n    }\n\n    fn minimal_snapshot() -> AgentRunSnapshot {\n        AgentRunSnapshot {\n            run_id: \"job-42\".to_string(),\n            parent: None,\n            source: RunSource::CoreJob,\n            state: RunState::Queued,\n            budget: BudgetSummary::default(),\n            terminal: None,\n            refs: Vec::new(),\n        }\n    }\n\n    #[test]\n    fn full_snapshot_round_trips() {\n        let snapshot = full_snapshot();\n        let json = serde_json::to_string(&snapshot).expect(\"serialize\");\n        let back: AgentRunSnapshot = serde_json::from_str(&json).expect(\"deserialize\");\n        assert_eq!(back, snapshot);\n        assert!(back.is_coherent());\n    }\n\n    #[test]\n    fn minimal_snapshot_round_trips_and_skips_empty_fields() {\n        let snapshot = minimal_snapshot();\n        let json = serde_json::to_string(&snapshot).expect(\"serialize\");\n        // Optional/empty fields stay off the wire entirely.\n        assert!(!json.contains(\"parent\"));\n        assert!(!json.contains(\"terminal\"));\n        assert!(!json.contains(\"refs\"));\n        assert!(!json.contains(\"token_budget\"));\n        let back: AgentRunSnapshot = serde_json::from_str(&json).expect(\"deserialize\");\n        assert_eq!(back, snapshot);\n        assert!(back.is_coherent());\n    }","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/protocol/src/agent_run.rs#L236-L272","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","solutions":["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.","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).","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.","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.","Re-run the specific test to confirm: `cargo test -p codewhale-protocol full_snapshot_round_trips`."],"exampleFix":"// before\npub struct AgentRunSnapshot {\n    pub run_id: String,\n    pub owners: std::collections::HashMap<RunId, String>, // non-string keys, RunId not Serialize\n}\n// after\npub struct AgentRunSnapshot {\n    pub run_id: String,\n    /// Serialized IDs only — JSON-compatible map keys.\n    #[serde(default, skip_serializing_if = \"std::collections::HashMap::is_empty\")]\n    pub owners: std::collections::HashMap<String, String>,\n}","handlingStrategy":"type-guard","validationCode":"// Compile-time guard that the whole snapshot type is JSON-serializable\nfn assert_json_roundtrip<T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(v: &T) {\n    let json = serde_json::to_string(v).expect(\"serialize\");\n    let back: T = serde_json::from_str(&json).expect(\"deserialize\");\n    assert_eq!(&back, v);\n}\nassert_json_roundtrip(&snapshot);","typeGuard":"fn serializable<T: serde::Serialize>(v: &T) -> Option<String> {\n    serde_json::to_string(v).ok()\n}","tryCatchPattern":"let json = serde_json::to_string(&snapshot)\n    .map_err(|e| format!(\"snapshot serialize failed: {e}\"))?; // keep the serde error, don't expect() it away","preventionTips":["Keep AgentRunSnapshot to serialized IDs, neutral enums, and scalars only — never handles, non-string map keys, or owner-internal types (the module doc mandates this).","Run `cargo test -p codewhale-protocol` after any change to snapshot field types or serde attributes.","Derive Serialize/Deserialize together on every new field type and on all nested enums.","Copy existing serde attribute patterns (`default` + `skip_serializing_if`) verbatim for new optional fields.","Use fully-qualified paths in skip_serializing_if so helper renames can't break them."],"tags":["rust","serde","serialization","json","test-panic"],"backgroundTag":"json-serialization-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}