{"record":{"id":"d3252ac3f5216e37","repo":"odysseus-dev/odysseus","slug":"expected-a-json-object","errorCode":null,"errorMessage":"Expected a JSON object","messagePattern":"Expected a JSON object","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/backup_routes.py","lineNumber":74,"sourceCode":"        filename = f\"odysseus_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json\"\n        return Response(\n            content=json.dumps(export_data, indent=2, ensure_ascii=False),\n            media_type=\"application/json\",\n            headers={\"Content-Disposition\": f\"attachment; filename={filename}\"},\n        )\n\n    @router.post(\"/api/import\")\n    async def import_data(request: Request):\n        \"\"\"Import user data from a previously exported JSON file. Merges with existing data.\"\"\"\n        require_admin(request)\n        user = get_current_user(request)\n        try:\n            body = await request.json()\n        except Exception:\n            raise HTTPException(400, \"Invalid JSON\")\n\n        if not isinstance(body, dict):\n            raise HTTPException(400, \"Expected a JSON object\")\n\n        imported = []\n\n        # ── Memories ──\n        if \"memories\" in body and isinstance(body[\"memories\"], list):\n            # Strict load: importing on top of an unreadable store would write\n            # only the incoming rows back and drop everything already saved.\n            try:\n                existing = memory_manager.load_all_for_update()\n            except MemoryStoreUnreadable as e:\n                logger.error(\"Refusing to import memories: %s\", e)\n                raise HTTPException(\n                    503, \"Memory store is temporarily unreadable — nothing was imported.\"\n                )\n            # Dedup against THIS user's own memories only. Using every tenant's\n            # rows (load_all) meant a memory whose text matched any other\n            # user's was silently skipped, so the importing user lost their own\n            # data. The full store is still saved back below.","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/backup_routes.py#L56-L92","documentation":"HTTP 400 raised by POST /api/import when the parsed body is not a JSON object (dict). Arrays, bare strings, numbers, or null parse successfully but fail isinstance(body, dict). The import merges top-level keys like 'memories', so a non-object body is structurally unusable.","triggerScenarios":"POST /api/import with a JSON array (e.g. the user wrapped the export in [...] or uploaded an inner list), a quoted string body '\"...\"', or literal null.","commonSituations":"User exports a list from another tool and tries to import it; copying an inner array from an export file instead of the whole object; jq misuse (jq '.memories' instead of '.').","solutions":["Upload the export file exactly as produced by /api/export — a top-level object.","If importing custom data, wrap it: {\"memories\": [...]} etc.","Inspect the first character of the file: '[' means you have a list, '{' means an object."],"exampleFix":"# before\n# file contains just the array:  [ {\"text\": \"...\"} ]\n\n# after\n# file contains the full object:  {\"memories\": [ {\"text\": \"...\"} ]}","handlingStrategy":"type-guard","validationCode":"const data = JSON.parse(text);\nif (typeof data !== 'object' || data === null || Array.isArray(data)) {\n  throw new Error('Export must be a JSON object, not a list');\n}","typeGuard":"const isJsonObject = (v: unknown): v is Record<string, unknown> =>\n  typeof v === 'object' && v !== null && !Array.isArray(v);","tryCatchPattern":"if (res.status === 400 && detail === 'Expected a JSON object') { showFileError('Use the full export file (an object)'); }","preventionTips":["Check the file starts with '{' before importing.","Never wrap the export in an array or extract an inner list with jq."],"tags":["validation","http-400","json","import","type-guard"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}