usestrix/strix · warning · TypeError

Updates must be a list of update objects

Error message

Updates must be a list of update objects

What it means

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.

Source

Thrown at strix/tools/todo/tools.py:169


def _normalize_bulk_updates(raw_updates: Any) -> list[dict[str, Any]]:
    if raw_updates is None:
        return []
    data: Any = raw_updates
    if isinstance(raw_updates, str):
        stripped = raw_updates.strip()
        if not stripped:
            return []
        try:
            data = json.loads(stripped)
        except json.JSONDecodeError as e:
            raise ValueError("Updates must be valid JSON") from e

    if isinstance(data, dict):
        data = [data]
    if not isinstance(data, list):
        raise TypeError("Updates must be a list of update objects")

    normalized: list[dict[str, Any]] = []
    for item in data:
        if not isinstance(item, dict):
            raise TypeError("Each update must be an object with todo_id")
        todo_id = item.get("todo_id") or item.get("id")
        if not todo_id:
            raise ValueError("Each update must include 'todo_id'")
        normalized.append(
            {
                "todo_id": str(todo_id).strip(),
                "title": item.get("title"),
                "description": item.get("description"),
                "priority": item.get("priority"),
                "status": item.get("status"),
            },
        )
    return normalized

View on GitHub (pinned to 8551339130)

Solutions

  1. Shape the payload as a list of objects: [{"todo_id": "t1", "status": "done"}].
  2. A single object is fine — it will be wrapped automatically — but bare scalars/strings are not.
  3. Check for double-encoded JSON (a string that itself contains a JSON string) when the decoded value is still a str.

Example fix

# before
bulk_update_todos(updates='"mark done"')

# after
bulk_update_todos(updates=[{"todo_id": "t1", "status": "done"}])
Defensive patterns

Strategy: type-guard

Validate before calling

import json

def coerce_updates(raw):
    data = json.loads(raw) if isinstance(raw, str) and raw.strip() else raw
    if isinstance(data, dict):
        data = [data]
    if not isinstance(data, list):
        raise TypeError("updates must decode to a list")
    return data

Type guard

def is_update_list(data: object) -> bool:
    return isinstance(data, list) or isinstance(data, dict)  # dict gets auto-wrapped

Try / catch

try:
    bulk_update_todos(updates=updates)
except TypeError as exc:
    if "list of update objects" in str(exc):
        updates = [updates] if isinstance(updates, dict) else updates
        raise  # scalars/strings cannot be repaired mechanically — surface it
    raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15). Data as JSON: /api/errors/5a733918db1e8823. Report an issue: GitHub.