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

  1. Update parse_search_response (or revert the response change) so it matches the frozen shape
  2. Adjust the frozen fixture only as part of a deliberate, documented schema change
  3. Log the serde error from the failing parse to pinpoint the mismatched field
  4. 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

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/704e407b22c16c9b. Report an issue: GitHub.