odysseus-dev/odysseus · error · HTTPException
Expected a JSON object
Error message
Expected a JSON object
What it means
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.
Source
Thrown at routes/backup_routes.py:74
filename = f"odysseus_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
return Response(
content=json.dumps(export_data, indent=2, ensure_ascii=False),
media_type="application/json",
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
@router.post("/api/import")
async def import_data(request: Request):
"""Import user data from a previously exported JSON file. Merges with existing data."""
require_admin(request)
user = get_current_user(request)
try:
body = await request.json()
except Exception:
raise HTTPException(400, "Invalid JSON")
if not isinstance(body, dict):
raise HTTPException(400, "Expected a JSON object")
imported = []
# ── Memories ──
if "memories" in body and isinstance(body["memories"], list):
# Strict load: importing on top of an unreadable store would write
# only the incoming rows back and drop everything already saved.
try:
existing = memory_manager.load_all_for_update()
except MemoryStoreUnreadable as e:
logger.error("Refusing to import memories: %s", e)
raise HTTPException(
503, "Memory store is temporarily unreadable — nothing was imported."
)
# Dedup against THIS user's own memories only. Using every tenant's
# rows (load_all) meant a memory whose text matched any other
# user's was silently skipped, so the importing user lost their own
# data. The full store is still saved back below.View on GitHub (pinned to f9235ebbf1)
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.
Example fix
# before
# file contains just the array: [ {"text": "..."} ]
# after
# file contains the full object: {"memories": [ {"text": "..."} ]} Defensive patterns
Strategy: type-guard
Validate before calling
const data = JSON.parse(text);
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
throw new Error('Export must be a JSON object, not a list');
} Type guard
const isJsonObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
Try / catch
if (res.status === 400 && detail === 'Expected a JSON object') { showFileError('Use the full export file (an object)'); } Prevention
- Check the file starts with '{' before importing.
- Never wrap the export in an array or extract an inner list with jq.
When it happens
Trigger: 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.
Common situations: 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 '.').
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/d3252ac3f5216e37.
Report an issue: GitHub.