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
- 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`.
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
- 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.
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
- serialize
- continue_goal with a wire-supplied schedule id still parses
- deserialize
- <dynamic: wrapped serde_json serialization error>
- <dynamic: wrapped serde_json serialization error>
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)