astrid-runtime/astrid · error

sparse SUMMARY deserializes

Error message

sparse SUMMARY deserializes

What it means

The same round-trip test also deserializes a minimal JSON object containing only session_id, message_count, and archived into SessionSummary with expect("sparse SUMMARY deserializes"). A panic means the always-present fields or their types changed, or a previously-optional field became required so the sparse payload no longer deserializes.

Source

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

        "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());
    assert!(s.archived);
}

#[test]
fn parse_session_field_null_is_none_for_404() {
    // Explicit null → None (the handler maps None to a 404).
    let null_reply = serde_json::json!({ "correlation_id": "c", "session": null });
    assert!(parse_session_field(&null_reply).unwrap().is_none());
    // Absent `session` → None too.
    let absent = serde_json::json!({ "correlation_id": "c" });
    assert!(parse_session_field(&absent).unwrap().is_none());
}

View on GitHub (pinned to affd8760f4)

Solutions

  1. Restore Option/#[serde(default)] on fields that must remain absent-able in sparse responses
  2. Update the sparse fixture if a field legitimately joined the always-present set
  3. Read the serde error in the panic to find the missing/mistyped field
  4. Keep the frozen wire contract documented so field optionality changes are deliberate

Example fix

// before
pub created_at: i64,
// after
#[serde(default)]
pub created_at: Option<i64>,
Defensive patterns

Strategy: type-guard

Validate before calling

assert!(sparse.get("session_id").is_some() && sparse.get("title").is_none(),
    "sparse fixture must omit optional fields");

Type guard

fn is_sparse_summary(v: &serde_json::Value) -> bool {
    v.get("session_id").is_some()
        && v.get("message_count").is_some()
        && v.get("archived").is_some()
}

Try / catch

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

Prevention

When it happens

Trigger: Making a field required that the sparse fixture omits (title, created_at, meta, etc.), changing types of session_id/message_count/archived, or removing #[serde(default)]/Option wrappers.

Common situations: Refactor tightening SessionSummary field optionality; switching archived from bool to an enum; changing message_count type; serde default attribute removed during cleanup.

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