clockworklabs/SpacetimeDB · error
Procedure return value failed to serialize to JSON
Error message
Procedure return value failed to serialize to JSON
What it means
When a procedure is called over WebSocket v1 with the text (JSON) protocol, the host serializes the return value with `serde_json::to_string(&SerializeWrapper(val)).expect("Procedure return value failed to serialize to JSON")`. serde_json errors on values JSON cannot represent — most famously maps with non-string keys — plus custom Serialize impls that return errors.
Source
Thrown at crates/core/src/client/messages.rs:853
ws_v1::ServerMessage::ProcedureResult(ws_v1::ProcedureResult {
status,
timestamp,
total_host_execution_duration,
request_id,
})
}
// Note that procedure returns are sent only to the caller, not broadcast to all subscribers,
// so we don't have to bother with memoizing the serialization the way we do for reducer args.
match protocol {
Protocol::Binary => ws_v1::FormatSwitch::Bsatn(convert(self, |val| {
bsatn::to_vec(&val)
.expect("Procedure return value failed to serialize to BSATN")
.into()
})),
Protocol::Text => ws_v1::FormatSwitch::Json(convert(self, |val| {
serde_json::to_string(&SerializeWrapper(val))
.expect("Procedure return value failed to serialize to JSON")
.into()
})),
}
}
}
View on GitHub (pinned to 524b4487d9)
Solutions
- Replace non-string map keys with strings (HashMap<String, T>) or a Vec of key/value pairs for JSON clients.
- Derive SpacetimeType/Serialize instead of hand-writing impls for return values.
- Republish the module and regenerate bindings after any return-type change.
Example fix
// before: JSON cannot serialize integer-keyed maps
fn counts(ctx: &ReducerContext) -> HashMap<u64, u64> { ... }
// after: use string keys or a vec of entries
#[derive(spacetimedb::SpacetimeType)]
pub struct CountEntry { pub id: String, pub count: u64 }
fn counts(ctx: &ReducerContext) -> Vec<CountEntry> { ... } Defensive patterns
Strategy: type-guard
Validate before calling
// Reject non-JSON-representable returns before exposing a procedure to text-protocol clients
#[test]
fn ret_json_ok() { assert!(serde_json::to_string(&sample_return()).is_ok()); } Type guard
trait JsonRepresentable: serde::Serialize {} // marker for types proven round-trippable by the test above Prevention
- Use string keys (or Vec of entries) instead of integer-keyed maps in return types.
- Derive Serialize/SpacetimeType; avoid custom impls on returns.
- Test both BSATN and JSON encodings in CI since clients pick the protocol.
When it happens
Trigger: Calling a procedure whose return type contains a map with integer/tuple keys, or a custom Serialize that fails, while the client subscribes with Protocol::Text (JSON WebSocket subprotocol).
Common situations: Returning HashMap<u32, T> or similar keyed maps to JSON-protocol clients; hand-written Serialize impls; schema/runtime skew after module edits without republishing.
Related errors
- Procedure return value failed to serialize to BSATN
- Procedure return value failed to serialize to BSATN
- cannot serialize refs without a typespace
- could not serialize result: object had neither a `ok` nor an
- Could not serialize sum type; unknown tag ${value.tag}
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/83379566daf58b09.
Report an issue: GitHub.