{"record":{"id":"5816b595cf564ab3","repo":"BloopAI/vibe-kanban","slug":"scratch-serialization-should-not-fail","errorCode":null,"errorMessage":"Scratch serialization should not fail","messagePattern":"Scratch serialization should not fail","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/services/src/services/events/patches.rs","lineNumber":108,"sourceCode":"                .expect(\"Workspace path should be valid\"),\n        })])\n    }\n}\n\n/// Helper functions for creating scratch-specific patches.\n/// All patches use path \"/scratch\" - filtering is done by matching id and payload type in the value.\npub mod scratch_patch {\n    use super::*;\n\n    const SCRATCH_PATH: &str = \"/scratch\";\n\n    /// Create patch for adding a new scratch\n    pub fn add(scratch: &Scratch) -> Patch {\n        Patch(vec![PatchOperation::Add(AddOperation {\n            path: SCRATCH_PATH\n                .try_into()\n                .expect(\"Scratch path should be valid\"),\n            value: serde_json::to_value(scratch).expect(\"Scratch serialization should not fail\"),\n        })])\n    }\n\n    /// Create patch for updating an existing scratch\n    pub fn replace(scratch: &Scratch) -> Patch {\n        Patch(vec![PatchOperation::Replace(ReplaceOperation {\n            path: SCRATCH_PATH\n                .try_into()\n                .expect(\"Scratch path should be valid\"),\n            value: serde_json::to_value(scratch).expect(\"Scratch serialization should not fail\"),\n        })])\n    }\n\n    /// Create patch for removing a scratch.\n    /// Uses Replace with deleted marker so clients can filter by id and payload type.\n    pub fn remove(scratch_id: Uuid, scratch_type_str: &str) -> Patch {\n        Patch(vec![PatchOperation::Replace(ReplaceOperation {\n            path: SCRATCH_PATH","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/crates/services/src/services/events/patches.rs#L90-L126","documentation":"This panic comes from `serde_json::to_value(scratch).expect(...)` inside `Patch::add` (crates/services/src/services/events/patches.rs:108). `serde_json::to_value` only fails when a type's Serialize impl errors — e.g. a custom serializer emitting a non-string map key or propagating an inner serialization error. The expectation is that `Scratch` is plain serializable data, so this indicates a broken Serialize implementation for `Scratch` or one of its payload variants.","triggerScenarios":"Calling `Patch::add(&scratch)` where serializing the `Scratch` (or its `payload`, including `scratch_type_str`-tagged payload types) returns `Err` — typically because a nested payload uses a serialization-impossible construct (non-string map keys, custom Serialize impls returning errors, f64 NaN/Infinity with restricted serde settings).","commonSituations":"Adding a new Scratch payload variant whose Serialize impl returns Err; using HashMap keys that aren't strings; NaN/Infinity floats in payload data; a downstream dependency changing serde behavior for a payload field.","solutions":["Inspect the Scratch value (especially `payload`) for data serde_json cannot represent (non-string map keys, NaN/Infinity floats); sanitize or remap such data before constructing the Scratch.","Review the `Scratch`/payload Serialize impls (including custom ones) and fix any that can return Err so serialization is total.","Temporarily replace `.expect` with `match`/`unwrap_err` logging to capture the real serde error message and target the failing field.","If a third-party payload type is the culprit, wrap it with a lossy serializer (`serde_json::to_value(&val).unwrap_or(Value::Null)`-style) or convert it to plain serde_json::Value before embedding."],"exampleFix":"// before\nvalue: serde_json::to_value(scratch).expect(\"Scratch serialization should not fail\"),\n// after\nvalue: serde_json::to_value(scratch).unwrap_or_else(|e| {\n    tracing::error!(error = ?e, \"failed to serialize scratch\");\n    serde_json::Value::Null\n}),","handlingStrategy":"type-guard","validationCode":"// Pre-flight check before building the patch:\nfn scratch_serializable(scratch: &Scratch) -> bool {\n    serde_json::to_value(scratch).is_ok()\n}","typeGuard":"fn is_json_safe(v: &serde_json::Value) -> bool {\n    use serde_json::Value::*;\n    match v {\n        Null | Bool(_) | Number(_) | String(_) => v.as_f64().map(|f| f.is_finite()).unwrap_or(true),\n        Array(a) => a.iter().all(is_json_safe),\n        Object(o) => o.values().all(is_json_safe),\n    }\n}","tryCatchPattern":"// .expect panics aren't catchable; prefer non-panicking construction:\nmatch serde_json::to_value(scratch) {\n    Ok(value) => { /* build PatchOperation::Add */ }\n    Err(e) => tracing::error!(error = ?e, \"scratch serialization failed\"),\n}","preventionTips":["Keep Scratch payloads plain serde_json-friendly data; avoid custom Serialize impls that can error.","Ban NaN/Infinity floats and non-string map keys in payload data.","Add a round-trip test (to_value then from_value) for every new Scratch payload variant.","Capture the serde error message first when debugging to identify the offending field."],"tags":["rust","panic","serde","serialization"],"backgroundTag":"serde-serialization-failed","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}