astrid-runtime/astrid · error

frozen SUMMARY deserializes

Error message

frozen SUMMARY deserializes

What it means

session_summary_round_trips_frozen_summary builds a full JSON object and deserializes it into SessionSummary with expect("frozen SUMMARY deserializes"). The test pins the frozen wire format of SessionSummary, so a panic means a serde field/type change broke backward compatibility with the frozen shape.

Source

Thrown at crates/astrid-gateway/src/routes/sessions_tests.rs:587

    assert_eq!(p["include_archived"], false);
}

#[test]
fn session_summary_round_trips_frozen_summary() {
    // The frozen SUMMARY shape, fully populated, then mostly-null.
    let full = serde_json::json!({
        "session_id": "default",
        "title": "Planning",
        "preview": "first user message",
        "last_message_preview": "latest line",
        "message_count": 12,
        "created_at": 1_719_000_000_i64,
        "updated_at": 1_719_000_100_i64,
        "archived": false,
        "parent_session_id": "old-id",
        "meta": "{\"k\":1}"
    });
    let s: SessionSummary = serde_json::from_value(full).expect("frozen SUMMARY deserializes");
    assert_eq!(s.session_id, "default");
    assert_eq!(s.title.as_deref(), Some("Planning"));
    assert_eq!(s.last_message_preview.as_deref(), Some("latest line"));
    assert!(!s.archived);
    assert_eq!(s.meta.as_deref(), Some("{\"k\":1}"));

    // Minimal/sparse element: only the always-present fields.
    let sparse = serde_json::json!({
        "session_id": "fresh",
        "message_count": 0,
        "archived": true
    });
    let s: SessionSummary = serde_json::from_value(sparse).expect("sparse SUMMARY deserializes");
    assert!(s.title.is_none());
    assert!(s.preview.is_none());
    assert!(s.last_message_preview.is_none());
    assert!(s.created_at.is_none());
    assert!(s.meta.is_none());

View on GitHub (pinned to affd8760f4)

Solutions

  1. Revert the serde change to SessionSummary or update the frozen fixture deliberately with a documented schema change
  2. Make the new field #[serde(default)] if it must be optional for old payloads
  3. Run the test with the serde error message to identify the offending field
  4. If the format change is intended, update this round-trip test and any frozen-shape docs together

Example fix

// before
pub meta: String,
// after
pub meta: Option<String>, // plus #[serde(default)] to keep frozen shape compatible
Defensive patterns

Strategy: try-catch

Validate before calling

assert!(full.get("session_id").map_or(false, |v| v.is_string()),
    "frozen summary fixture malformed");

Type guard

fn has_required_summary(v: &serde_json::Value) -> bool {
    ["session_id", "message_count", "archived"]
        .iter().all(|k| v.get(k).is_some())
}

Try / catch

let s: SessionSummary = serde_json::from_value(full)
    .unwrap_or_else(|e| panic!("frozen SUMMARY deserializes: {e}"));

Prevention

When it happens

Trigger: Changing SessionSummary fields or types (session_id, title, last_message_preview, message_count, created_at, updated_at, archived, parent_session_id, meta) so the full frozen JSON no longer deserializes.

Common situations: Renaming a field or making an optional field required; changing i64 timestamps to another type; adding a new required field absent from the frozen fixture; serde attribute changes (deny_unknown_fields, default removal).

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/689f3d930effadb1. Report an issue: GitHub.