astrid-runtime/astrid · error
frozen search shape deserializes
Error message
frozen search shape deserializes
What it means
parse_search_response_round_trips_frozen_shape deserializes a frozen search-response JSON (results array with session_id/title/match_count/updated_at and next_cursor) via parse_search_response, expecting success. A panic means the search response shape drifted from the frozen contract the test pins.
Source
Thrown at crates/astrid-gateway/src/routes/sessions_tests.rs:679
}
#[test]
fn parse_search_response_round_trips_frozen_shape() {
let body = serde_json::json!({
"correlation_id": "c",
"results": [
{
"session_id": "s1",
"title": "Trip planning",
"snippet": "…book the flight…",
"match_count": 2,
"updated_at": 1_719_000_000_i64
},
{ "session_id": "s2", "match_count": 1 }
],
"next_cursor": "page-2"
});
let parsed = parse_search_response(body).expect("frozen search shape deserializes");
assert_eq!(parsed.results.len(), 2);
assert_eq!(parsed.next_cursor.as_deref(), Some("page-2"));
let first = &parsed.results[0];
assert_eq!(first.session_id, "s1");
assert_eq!(first.title.as_deref(), Some("Trip planning"));
assert_eq!(first.match_count, 2);
assert_eq!(first.updated_at, Some(1_719_000_000));
let second = &parsed.results[1];
assert!(second.title.is_none());
assert!(second.snippet.is_none());
assert!(second.updated_at.is_none());
assert_eq!(second.match_count, 1);
}
#[test]
fn parse_search_response_null_next_cursor_is_last_page() {
let body = serde_json::json!({ "results": [], "next_cursor": null });
let parsed = parse_search_response(body).expect("empty page deserializes");View on GitHub (pinned to affd8760f4)
Solutions
- Update parse_search_response (or revert the response change) so it matches the frozen shape
- Adjust the frozen fixture only as part of a deliberate, documented schema change
- Log the serde error from the failing parse to pinpoint the mismatched field
- Keep search response pagination fields (next_cursor) backward compatible
Example fix
// before pub nextCursor: Option<String>, // after #[serde(rename = "next_cursor")] pub next_cursor: Option<String>,
Defensive patterns
Strategy: try-catch
Validate before calling
assert!(body.get("results").map_or(false, serde_json::Value::is_array),
"search reply missing results array"); Type guard
fn is_search_reply(v: &serde_json::Value) -> bool {
v.get("results").map_or(false, serde_json::Value::is_array)
} Try / catch
let parsed = parse_search_response(&body)
.unwrap_or_else(|e| panic!("frozen search shape deserializes: {e}; body={body}")); Prevention
- Pin the search wire shape with this frozen fixture and keep it in CI
- Rename pagination fields only with a documented schema bump
- Include the raw body in parser error messages for fast diagnosis
When it happens
Trigger: Changing SearchResponse/its result item fields or types (results, next_cursor, session_id, title, match_count, updated_at) so the frozen body fails to deserialize in parse_search_response.
Common situations: Renaming next_cursor or results; changing match_count type; adding a required field to search result items; cursor encoding changes reflected in the deserializer.
Understand the failure class
Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.
Related errors
- reply deserializes
- tool_describe payload is not valid JSON or has an unexpected
- frozen SUMMARY deserializes
- sparse SUMMARY deserializes
- present session
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/704e407b22c16c9b.
Report an issue: GitHub.