{"record":{"id":"fefb65053c47f22d","repo":"usestrix/strix","slug":"each-update-must-be-an-object-with-todo-id","errorCode":null,"errorMessage":"Each update must be an object with todo_id","messagePattern":"Each update must be an object with todo_id","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"warning","filePath":"strix/tools/todo/tools.py","lineNumber":174,"sourceCode":"    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\n\n\ndef _normalize_bulk_todos(raw_todos: Any) -> list[dict[str, Any]]:\n    if raw_todos is None:\n        return []","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/tools/todo/tools.py#L156-L192","documentation":"Inside the updates list, every element must be a dict; a non-dict element raises TypeError('Each update must be an object with todo_id'). This runs before the todo_id presence check, so ['t1'] or [42] fails here even though the message also mentions todo_id. Only after passing this check is item.get('todo_id')/'id' looked up.","triggerScenarios":"Passing a mixed list like [{...}, \"t2\"] or a list of id strings ['t1','t2']. Each element must be an object; ids must be wrapped: [{\"todo_id\": \"t2\"}].","commonSituations":"LLM abbreviating the schema to bare ids; callers converting between list-of-ids and list-of-patches; JSON arrays of strings from external tooling.","solutions":["Wrap every element as an object: [{\"todo_id\": \"t1\", ...}, {\"todo_id\": \"t2\", ...}].","If starting from bare ids, map them: ids.map(id => ({\"todo_id\": id})).","For LLM tool schemas, require an array of objects and show an example in the description."],"exampleFix":"# before\nbulk_update_todos(updates=\"[\\\"t1\\\", \\\"t2\\\"]\")\n\n# after\nbulk_update_todos(updates='[{\"todo_id\": \"t1\"}, {\"todo_id\": \"t2\"}]')","handlingStrategy":"type-guard","validationCode":"def all_elements_are_objects(data: list) -> bool:\n    return all(isinstance(item, dict) for item in data)\n\nif not all_elements_are_objects(updates):\n    updates = [{\"todo_id\": item} if isinstance(item, str) else item for item in updates]","typeGuard":"def is_normalized_updates(items: object) -> bool:\n    return (\n        isinstance(items, list)\n        and all(isinstance(i, dict) and (i.get(\"todo_id\") or i.get(\"id\")) for i in items)\n    )","tryCatchPattern":"try:\n    bulk_update_todos(updates=updates)\nexcept TypeError as exc:\n    if \"object with todo_id\" in str(exc):\n        updates = [u if isinstance(u, dict) else {\"todo_id\": u} for u in updates]\n        bulk_update_todos(updates=updates)\n    else:\n        raise","preventionTips":["Wrap bare ids into objects before submitting: ids -> [{'todo_id': id}].","Specify 'array of objects' in LLM tool schemas with a concrete example.","Reused id lists across systems should be converted to patch lists at the 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"}