shareAI-lab/learn-claude-code · error · ValueError

todos must be a list or JSON array string

Error message

todos must be a list or JSON array string

What it means

Raised by TodoManager.update in s05_todo_write/code.py when the todos argument is a string that is neither parseable JSON nor parseable by ast.literal_eval. The module accepts both formats because LLM tool callers frequently emit Python-literal lists with single quotes; when both parsers fail, this wrapper error is raised with the original parse error chained as __cause__.

Source

Thrown at s05_todo_write/code.py:122

    except Exception as e:
        return f"Error: {e}"


# -- New in s05: structured state the model updates --

class TodoManager:
    def __init__(self):
        self.items: list[dict] = []

    def update(self, todos: list | str) -> str:
        if isinstance(todos, str):
            try:
                todos = json.loads(todos)
            except json.JSONDecodeError:
                try:
                    todos = ast.literal_eval(todos)
                except (SyntaxError, ValueError) as e:
                    raise ValueError("todos must be a list or JSON array string") from e

        if not isinstance(todos, list):
            raise ValueError("todos must be a list")
        if len(todos) > 20:
            raise ValueError("Max 20 todos allowed")

        validated = []
        in_progress_count = 0
        for index, todo in enumerate(todos):
            if not isinstance(todo, dict):
                raise ValueError(f"todos[{index}] must be an object")

            content = str(todo.get("content", "")).strip()
            status = str(todo.get("status", "pending")).lower()
            if not content:
                raise ValueError(f"todos[{index}] requires content")
            if status not in ("pending", "in_progress", "completed"):
                raise ValueError(f"todos[{index}] has invalid status '{status}'")

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Pass the list as a native JSON array (the tool schema's array type) instead of a string
  2. Strip markdown fences and surrounding prose before calling: s.strip().strip('`').removeprefix('json')
  3. Inspect e.__cause__ to see the exact parse location and fix that character
  4. Validate client-side with json.loads first and retry generation on failure

Example fix

// before
TODOS.update("```json\n[{\"content\": \"x\", \"status\": \"pending\"}]\n```")
// after
TODOS.update([{"content": "x", "status": "pending"}])
Defensive patterns

Strategy: validation

Validate before calling

import json

def parse_todos(raw):
    if isinstance(raw, str):
        s = raw.strip()
        if s.startswith('```'):
            s = s.strip('`').removeprefix('json').strip()
        raw = json.loads(s)  # raises early with a clear JSON error
    assert isinstance(raw, list)
    return raw

Try / catch

try:
    TODOS.update(raw)
except ValueError as e:
    if 'must be a list or JSON array' in str(e):
        raw = json.loads(strip_fences(raw))
        TODOS.update(raw)
    else:
        raise

Prevention

When it happens

Trigger: Calling update with '[{"content": "x",]' (trailing comma, invalid JSON and invalid Python), a string containing markdown fences like "```json [...]"", truncated JSON cut off by an output token limit, or a bare word like "none".

Common situations: LLM tool calls wrapping the array in prose or code fences; single quotes handled fine by literal_eval but mixed quoting ("content": 'x') failing both parsers; response truncation producing half a JSON array.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/052ff9f341a9cf37. Report an issue: GitHub.