{"record":{"id":"5a733918db1e8823","repo":"usestrix/strix","slug":"updates-must-be-a-list-of-update-objects","errorCode":null,"errorMessage":"Updates must be a list of update objects","messagePattern":"Updates must be a list of update objects","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"warning","filePath":"strix/tools/todo/tools.py","lineNumber":169,"sourceCode":"\n\ndef _normalize_bulk_updates(raw_updates: Any) -> list[dict[str, Any]]:\n    if raw_updates is None:\n        return []\n    data: Any = raw_updates\n    if isinstance(raw_updates, str):\n        stripped = raw_updates.strip()\n        if not stripped:\n            return []\n        try:\n            data = json.loads(stripped)\n        except json.JSONDecodeError as e:\n            raise ValueError(\"Updates must be valid JSON\") from e\n\n    if isinstance(data, dict):\n        data = [data]\n    if not isinstance(data, list):\n        raise TypeError(\"Updates must be a list of update objects\")\n\n    normalized: list[dict[str, Any]] = []\n    for item in data:\n        if not isinstance(item, dict):\n            raise TypeError(\"Each update must be an object with todo_id\")\n        todo_id = item.get(\"todo_id\") or item.get(\"id\")\n        if not todo_id:\n            raise ValueError(\"Each update must include 'todo_id'\")\n        normalized.append(\n            {\n                \"todo_id\": str(todo_id).strip(),\n                \"title\": item.get(\"title\"),\n                \"description\": item.get(\"description\"),\n                \"priority\": item.get(\"priority\"),\n                \"status\": item.get(\"status\"),\n            },\n        )\n    return normalized","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/tools/todo/tools.py#L151-L187","documentation":"After optional JSON decoding, _normalize_bulk_updates requires the updates payload to be a list (a single dict is auto-wrapped into a one-element list). Any other JSON type — string, number, null after decoding, boolean — raises TypeError('Updates must be a list of update objects'). Note: a JSON string input that decodes to a non-list also lands here.","triggerScenarios":"Passing updates='\"done\"', updates='42', or a decoded non-list value. Passing a single dict {\"todo_id\": ...} is accepted (wrapped); a list of objects is accepted; everything else raises TypeError.","commonSituations":"LLM sends a bare string status instead of an object; caller JSON-encodes twice so the decoded value is a string; null/None slipped through after earlier processing.","solutions":["Shape the payload as a list of objects: [{\"todo_id\": \"t1\", \"status\": \"done\"}].","A single object is fine — it will be wrapped automatically — but bare scalars/strings are not.","Check for double-encoded JSON (a string that itself contains a JSON string) when the decoded value is still a str."],"exampleFix":"# before\nbulk_update_todos(updates='\"mark done\"')\n\n# after\nbulk_update_todos(updates=[{\"todo_id\": \"t1\", \"status\": \"done\"}])","handlingStrategy":"type-guard","validationCode":"import json\n\ndef coerce_updates(raw):\n    data = json.loads(raw) if isinstance(raw, str) and raw.strip() else raw\n    if isinstance(data, dict):\n        data = [data]\n    if not isinstance(data, list):\n        raise TypeError(\"updates must decode to a list\")\n    return data","typeGuard":"def is_update_list(data: object) -> bool:\n    return isinstance(data, list) or isinstance(data, dict)  # dict gets auto-wrapped","tryCatchPattern":"try:\n    bulk_update_todos(updates=updates)\nexcept TypeError as exc:\n    if \"list of update objects\" in str(exc):\n        updates = [updates] if isinstance(updates, dict) else updates\n        raise  # scalars/strings cannot be repaired mechanically — surface it\n    raise","preventionTips":["Always send an array (or a single object) — never a bare scalar or string.","Watch for double-encoded JSON where the decoded value is still a string.","Validate the decoded shape with isinstance checks at your boundary."],"tags":["todo","json","type-error","validation"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}