Hmbown/CodeWhale · error · std::io::Error

(serde_json deserialization error wrapped as…

Error message

(serde_json deserialization error wrapped as io::ErrorKind::InvalidData)

What it means

read_evidence_metadata reads a JSON sidecar (the evidence artifact metadata file) for a given session/handle pair and deserializes it into EvidenceArtifact. When serde_json fails to parse or validate the file contents, the error is wrapped as an io::Error with ErrorKind::InvalidData. This means the file exists and was readable, but its bytes are not valid JSON for the EvidenceArtifact schema.

Solutions

  1. Inspect the metadata file at the resolved session artifact path and validate it as JSON (jq or serde_json::from_slice in a test) to see the exact serde message wrapped in the io error.
  2. Delete or restore the corrupt sidecar so the owning tool regenerates it on next evidence write.
  3. If schema drift is the cause, migrate or discard artifacts written by the older version rather than editing files in place.
  4. Wrap the call to treat InvalidData as 'artifact unreadable' and fall back to regenerating the evidence rather than failing the whole operation.

Example fix

// before
let artifact = read_evidence_metadata(&session_id, &handle)?;
// after
let artifact = match read_evidence_metadata(&session_id, &handle) {
    Ok(a) => a,
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
        // corrupt/legacy sidecar: regenerate or skip
        regenerate_or_skip(&session_id, &handle)?
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: try-catch

Validate before calling

fn evidence_metadata_readable(path: &std::path::Path) -> bool {
    std::fs::read(path).map(|raw| serde_json::from_slice::<serde_json::Value>(&raw).is_ok()).unwrap_or(false)
}

Type guard

fn is_valid_evidence_json(raw: &[u8]) -> bool {
    serde_json::from_slice::<crate::tools::EvidenceArtifact>(raw).is_ok()
}

Try / catch

match read_evidence_metadata(session_id, handle) {
    Ok(a) => a,
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => fallback_regenerate(),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling read_evidence_metadata(session_id, handle) where the metadata file at the session-scoped evidence path exists but contains corrupt, truncated, hand-edited, or schema-mismatched JSON (e.g. missing required fields of EvidenceArtifact).

Common situations: A previous write was interrupted mid-file; a user or script edited the sidecar by hand; the schema_version of EvidenceArtifact changed between app versions so old files no longer deserialize; disk corruption or a partially synced file.

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 Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/5fdeda25c83c2b84. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tools/large_output_router.rs:314

pub fn publish_evidence_metadata(
    session_id: &str,
    artifact: &EvidenceArtifact,
) -> io::Result<PathBuf> {
    let bytes = serde_json::to_vec_pretty(artifact)
        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
    crate::artifacts::write_session_relative_immutable(
        session_id,
        &evidence_metadata_relative_path(&artifact.handle),
        &bytes,
    )
}

pub fn read_evidence_metadata(session_id: &str, handle: &str) -> io::Result<EvidenceArtifact> {
    let relative = evidence_metadata_relative_path(handle);
    let path = crate::artifacts::session_artifact_absolute_path(session_id, &relative)
        .ok_or_else(|| io::Error::new(io::ErrorKind::PermissionDenied, "invalid evidence owner"))?;
    let raw = std::fs::read(path)?;
    serde_json::from_slice(&raw).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}

#[must_use]
pub fn unix_millis_now() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis()
        .try_into()
        .unwrap_or(u64::MAX)
}

#[must_use]
pub fn evidence_is_expired(artifact: &EvidenceArtifact, now_ms: u64) -> bool {
    artifact.retention_state == EvidenceRetentionState::Expired
        || now_ms > artifact.retain_until_unix_ms
}

View on GitHub (pinned to 433685b202)