{"record":{"id":"bb1fee759f53bf27","repo":"Hmbown/CodeWhale","slug":"deserialize","errorCode":null,"errorMessage":"deserialize","messagePattern":"deserialize","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/protocol/src/agent_run.rs","lineNumber":255,"sourceCode":"    }\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    }\n","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/protocol/src/agent_run.rs#L237-L273","documentation":"This is the panic from `serde_json::from_str::<AgentRunSnapshot>(&json).expect(\"deserialize\")` in `full_snapshot_round_trips` at crates/protocol/src/agent_run.rs:255. The test first serializes a fully-populated snapshot and then requires the JSON to deserialize back into an identical `AgentRunSnapshot`. It panics when the produced JSON cannot be parsed into the type — missing required (non-`#[serde(default)]`) fields, enum strings that do not match any snake_case variant, or type mismatches (e.g. a number where a string is expected). Because the payload here is produced by the same type's serializer, a panic usually means required fields are absent (some field gained `skip_serializing_if` without `default`) or an enum's `rename_all = \"snake_case\"` contract was broken so the written variant name no longer parses.","triggerScenarios":"Calling `serde_json::from_str::<AgentRunSnapshot>` on JSON where: (1) a field without `#[serde(default)]` is missing (typical cause: a required field gained `skip_serializing_if = ...` without `default`, so serialization omits it and deserialization then rejects the missing key); (2) an enum field contains a string not matching a variant — e.g. \"Running\" instead of \"running\" after dropping `#[serde(rename_all = \"snake_case\")]` from `RunState`/`RunSource`/`TerminalOutcome`/`ReceiptKind`; (3) a field's JSON type changed (string vs number, object vs array); (4) malformed/hand-written JSON is fed directly, e.g. replaying an old ledger payload missing a required key.","commonSituations":"A contributor adds a non-optional field to `AgentRunSnapshot` (or moves `default`/`skip_serializing_if` around) and the round-trip test panics with \"deserialize\" at line 255. Also common when wire-format migrations change enum casing or rename fields: stored JSON from an older version no longer deserializes (the sibling receipt test in fleet.rs shows the intended pattern — `#[serde(default)]` keeps legacy payloads readable).","solutions":["Read the serde_json error detail: replace `.expect(\"deserialize\")` temporarily with a closure that prints the error (serde errors name the exact missing field or wrong type at a path) — this pinpoints the offending field immediately.","For every field missing from the payload, add `#[serde(default)]` (or `#[serde(default, skip_serializing_if = \"Option::is_none\")]` for optional ones) so omission on the wire is tolerated, matching how `budget`, `parent`, `terminal`, and `refs` are already annotated.","If the failure is an unknown enum string, confirm `#[serde(rename_all = \"snake_case\")]` is intact on all four enums and that the JSON uses the lowercase wire names the `enum_wire_names_are_snake_case_and_stable` test pins.","If deserializing hand-written or legacy JSON rather than freshly serialized output, add `#[serde(default)]` on newly required fields or a `#[serde(alias = \"old_name\")]` for renames so old payloads still parse.","Re-run: `cargo test -p codewhale-protocol full_snapshot_round_trips`."],"exampleFix":"// before\npub struct AgentRunSnapshot {\n    #[serde(skip_serializing_if = \"Option::is_none\")] // omitting parent breaks deserialize\n    pub parent: Option<String>,\n    pub state: RunState, // missing default: legacy JSON without `state` fails\n}\n// after\npub struct AgentRunSnapshot {\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub parent: Option<String>,\n    #[serde(default)]\n    pub state: RunState,\n}","handlingStrategy":"validation","validationCode":"// Validate a payload before deserializing into AgentRunSnapshot\nfn can_deserialize_snapshot(json: &str) -> bool {\n    serde_json::from_str::<crate::agent_run::AgentRunSnapshot>(json).is_ok()\n}\nif !can_deserialize_snapshot(&json) { /* handle legacy/invalid payload */ }","typeGuard":"fn parse_snapshot(json: &str) -> Option<crate::agent_run::AgentRunSnapshot> {\n    serde_json::from_str(json).ok()\n}","tryCatchPattern":"match serde_json::from_str::<AgentRunSnapshot>(&json) {\n    Ok(snap) => use(snap),\n    Err(e) => log::warn!(\"snapshot deserialize failed: {e}\"), // error names the missing field / bad enum\n}","preventionTips":["Pair every skip_serializing_if with #[serde(default)] so anything omitted from the wire can still be read back.","Never remove #[serde(default)] from parent/budget/terminal/refs — minimal payloads legitimately omit them.","Keep #[serde(rename_all = \"snake_case\")] on RunSource/RunState/TerminalOutcome/ReceiptKind; the stable-wire-names test pins this.","When adding fields to a persisted schema, default them so older stored JSON keeps deserializing (see the legacy-receipt pattern in fleet.rs).","Read the serde error message — it names the exact field and JSON path — instead of collapsing it with expect()."],"tags":["rust","serde","deserialization","json","wire-format"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T10:30:35.592Z"}