{"record":{"id":"6c9fabc287aebbb3","repo":"headroomlabs-ai/headroom","slug":"items-json-must-be-a-json-array-got","errorCode":null,"errorMessage":"items_json must be a JSON array, got {}","messagePattern":"items_json must be a JSON array, got (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/headroom-py/src/lib.rs","lineNumber":824,"sourceCode":"    fn crush_array_json<'py>(\n        &self,\n        py: Python<'py>,\n        items_json: &str,\n        query: &str,\n        bias: f64,\n    ) -> Bound<'py, PyDict> {\n        // GIL-release pattern: own the inputs, do all heavy compute\n        // (JSON parse, crush, re-serialize) without the GIL, then\n        // re-acquire to build the PyDict from the owned outputs.\n        let items_json = items_json.to_string();\n        let query = query.to_string();\n        let (kept_json, ccr_hash, dropped_summary, strategy_info, compacted, compaction_kind) = py\n            .detach(|| {\n                let parsed: serde_json::Value = serde_json::from_str(&items_json)\n                    .unwrap_or_else(|e| panic!(\"items_json must be JSON: {e}\"));\n                let items = match parsed {\n                    serde_json::Value::Array(a) => a,\n                    other => panic!(\"items_json must be a JSON array, got {}\", type_name(&other)),\n                };\n                let result = self.inner.crush_array(&items, &query, bias);\n                let kept_json = serde_json::to_string(&serde_json::Value::Array(result.items))\n                    .expect(\"serialize kept items\");\n                (\n                    kept_json,\n                    result.ccr_hash,\n                    result.dropped_summary,\n                    result.strategy_info,\n                    result.compacted,\n                    result.compaction_kind,\n                )\n            });\n        build_crush_array_dict(\n            py,\n            kept_json,\n            ccr_hash,\n            dropped_summary,","sourceCodeStart":806,"sourceCodeEnd":842,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/crates/headroom-py/src/lib.rs#L806-L842","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Extract the array before calling: pass envelope['data'] / the inner list, not the wrapper object.","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.","Guard on the Python side: json.loads then isinstance(x, list) before the call.","Check for double encoding — json.dumps(json.dumps(x)) produces a string-typed top level."],"exampleFix":"# before\nresult = hr.crush_array_json(json.dumps({\"data\": items}), query, 0.5)\n\n# after\nresult = hr.crush_array_json(json.dumps(items), query, 0.5)","handlingStrategy":"type-guard","validationCode":"# Validate top-level shape before calling\nimport json\ndef as_json_array(items_json: str) -> list | None:\n    try:\n        v = json.loads(items_json)\n    except json.JSONDecodeError:\n        return None\n    return v if isinstance(v, list) else None","typeGuard":"def is_json_array_string(s: str) -> bool:\n    try:\n        return isinstance(json.loads(s), list)\n    except (json.JSONDecodeError, TypeError):\n        return False","tryCatchPattern":"if not is_json_array_string(items_json):\n    raise ValueError(\"items_json must serialize a JSON array\")\ntry:\n    result = hr.crush_array_json(items_json, query, bias)\nexcept RuntimeError as e:\n    raise ValueError(f\"crush_array_json failed: {e}\") from e","preventionTips":["Unwrap envelope objects ({'data': [...]}) before serializing.","Use compact_document_json for document-shaped payloads, crush_array_json for row arrays.","Watch for double json.dumps producing a string top-level."],"tags":["python","json","pyo3","ffi","type-mismatch"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}