headroomlabs-ai/headroom · error

items_json must be a JSON array, got {}

Error message

items_json must be a JSON array, got {}

What it means

Python-binding panic from crush_array_json: the string parsed as valid JSON, but the top-level value is not an array (type_name of the actual value is embedded). The Rust API is crush_array — it iterates a JSON array of items; objects, strings, numbers, or null at the top level are rejected loudly inside the GIL-released closure.

Source

Thrown at crates/headroom-py/src/lib.rs:824

    fn crush_array_json<'py>(
        &self,
        py: Python<'py>,
        items_json: &str,
        query: &str,
        bias: f64,
    ) -> Bound<'py, PyDict> {
        // GIL-release pattern: own the inputs, do all heavy compute
        // (JSON parse, crush, re-serialize) without the GIL, then
        // re-acquire to build the PyDict from the owned outputs.
        let items_json = items_json.to_string();
        let query = query.to_string();
        let (kept_json, ccr_hash, dropped_summary, strategy_info, compacted, compaction_kind) = py
            .detach(|| {
                let parsed: serde_json::Value = serde_json::from_str(&items_json)
                    .unwrap_or_else(|e| panic!("items_json must be JSON: {e}"));
                let items = match parsed {
                    serde_json::Value::Array(a) => a,
                    other => panic!("items_json must be a JSON array, got {}", type_name(&other)),
                };
                let result = self.inner.crush_array(&items, &query, bias);
                let kept_json = serde_json::to_string(&serde_json::Value::Array(result.items))
                    .expect("serialize kept items");
                (
                    kept_json,
                    result.ccr_hash,
                    result.dropped_summary,
                    result.strategy_info,
                    result.compacted,
                    result.compaction_kind,
                )
            });
        build_crush_array_dict(
            py,
            kept_json,
            ccr_hash,
            dropped_summary,

View on GitHub (pinned to 322425c43b)

Solutions

  1. Extract the array before calling: pass envelope['data'] / the inner list, not the wrapper object.
  2. If the input is a whole document (mixed shapes, nested objects), call compact_document_json instead — its doc says it is for document-shape compaction rather than statistical row drop.
  3. Guard on the Python side: json.loads then isinstance(x, list) before the call.
  4. Check for double encoding — json.dumps(json.dumps(x)) produces a string-typed top level.

Example fix

# before
result = hr.crush_array_json(json.dumps({"data": items}), query, 0.5)

# after
result = hr.crush_array_json(json.dumps(items), query, 0.5)
Defensive patterns

Strategy: type-guard

Validate before calling

# Validate top-level shape before calling
import json
def as_json_array(items_json: str) -> list | None:
    try:
        v = json.loads(items_json)
    except json.JSONDecodeError:
        return None
    return v if isinstance(v, list) else None

Type guard

def is_json_array_string(s: str) -> bool:
    try:
        return isinstance(json.loads(s), list)
    except (json.JSONDecodeError, TypeError):
        return False

Try / catch

if not is_json_array_string(items_json):
    raise ValueError("items_json must serialize a JSON array")
try:
    result = hr.crush_array_json(items_json, query, bias)
except RuntimeError as e:
    raise ValueError(f"crush_array_json failed: {e}") from e

Prevention

When it happens

Trigger: Passing a JSON object like '{"items": [...]}' instead of the bare array '[...]'; passing a JSON string '"[1,2]"' (double-encoded); passing 'null' or a bare number; a document-shaped payload where only the nested array should be crushed.

Common situations: Wrapping responses in an envelope ({data: [...]}) and passing the whole envelope; double-serialization bugs (json.dumps applied twice); intending compact_document_json (the lossless walker for document shapes) but calling crush_array_json.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/6c9fabc287aebbb3. Report an issue: GitHub.