{"record":{"id":"c26234794f08890b","repo":"shareAI-lab/learn-claude-code","slug":"expected-a-json-list","errorCode":null,"errorMessage":"expected a JSON list","messagePattern":"expected a JSON list","errorType":"console","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"s12_cron_scheduler/code.py","lineNumber":378,"sourceCode":"            if job.durable\n        ]\n        temporary = DURABLE_PATH.with_name(\n            f\"{DURABLE_PATH.name}.{os.getpid()}.{threading.get_ident()}.tmp\"\n        )\n        try:\n            temporary.write_text(json.dumps(payload, indent=2))\n            os.replace(temporary, DURABLE_PATH)\n        finally:\n            temporary.unlink(missing_ok=True)\n\n\ndef load_durable_jobs():\n    if not DURABLE_PATH.exists():\n        return\n    try:\n        payload = json.loads(DURABLE_PATH.read_text())\n        if not isinstance(payload, list):\n            raise ValueError(\"expected a JSON list\")\n    except (OSError, json.JSONDecodeError, ValueError) as error:\n        print(f\"  [cron] could not load {DURABLE_PATH.name}: {error}\")\n        return\n\n    loaded = 0\n    with cron_lock:\n        for item in payload:\n            try:\n                job = CronJob(**item)\n                error = validate_cron(job.cron)\n                if error:\n                    raise ValueError(error)\n                if not job.id.startswith(\"cron_\"):\n                    raise ValueError(\"invalid job ID\")\n                if not job.prompt.strip():\n                    raise ValueError(\"prompt cannot be empty\")\n            except (TypeError, ValueError) as error:\n                print(f\"  [cron] skipped invalid saved job: {error}\")","sourceCodeStart":360,"sourceCodeEnd":396,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s12_cron_scheduler/code.py#L360-L396","documentation":"Raised by load_durable_jobs() in s12_cron_scheduler/code.py:378 when the durable jobs file (.scheduled_tasks.json in the workspace) parses as JSON but its top level is not a list. CronJob records are stored as a JSON array, one object per job, so an object at the top level (e.g. a single job, or a dict keyed by job id) is a schema violation. The error is caught together with OSError and JSONDecodeError, logged as 'could not load .scheduled_tasks.json: expected a JSON list', and loading aborts — the scheduler then simply has no durable jobs.","triggerScenarios":"Hand-editing .scheduled_tasks.json into {\"cron_ab12cd34\": {...}} or a single {...} job object; an external tool writing a dict-keyed format; a partial/interrupted write leaving valid JSON of the wrong shape (unlikely, since persistence uses atomic os.replace, so this is mainly manual edits or foreign writers).","commonSituations":"Users merging or 'cleaning up' the file by hand; a different version of the tool once persisted a dict; scripts that append to the file with a different structure.","solutions":["Restore the array shape: the file must be a JSON list of CronJob objects like [{\"id\": \"cron_…\", \"cron\": \"…\", \"prompt\": \"…\", ...}].","If you have a dict-keyed export, convert it: json.dump(list(d.values()), f).","Prefer recreating jobs through schedule_job() (which persists correctly) instead of editing the file, and keep a backup before manual edits."],"exampleFix":"# before: .scheduled_tasks.json\n{\"cron_ab12cd34\": {\"id\": \"cron_ab12cd34\", \"cron\": \"*/5 * * * *\", \"prompt\": \"hi\"}}\n\n# after\n[{\"id\": \"cron_ab12cd34\", \"cron\": \"*/5 * * * *\", \"prompt\": \"hi\", \"recurring\": true, \"durable\": true, \"pending_delivery\": false}]","handlingStrategy":"validation","validationCode":"import json\nfrom pathlib import Path\n\ndef durable_file_is_valid(path: Path) -> bool:\n    try:\n        return isinstance(json.loads(path.read_text()), list)\n    except (OSError, json.JSONDecodeError):\n        return False","typeGuard":"def is_job_list(payload) -> bool:\n    return isinstance(payload, list) and all(isinstance(j, dict) for j in payload)","tryCatchPattern":"try:\n    payload = json.loads(path.read_text())\n    assert isinstance(payload, list)\nexcept (ValueError, AssertionError):\n    backup_and_recreate(path)  # the loader only logs, so repair before restart","preventionTips":["Never hand-edit .scheduled_tasks.json; use schedule_job().","Keep backups before any manual merge of the durable file.","Verify top-level shape with json.loads + isinstance(list) after any external tool touches the file."],"tags":["cron","json","persistence","configuration"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}