odysseus-dev/odysseus · error · HTTPException
Body must be a JSON object
Error message
Body must be a JSON object
What it means
400 raised at routes/model_routes.py:2379 in the bulk hidden/pinned update handler when `await request.json()` succeeds but the parsed top-level value is not a JSON object (dict). The handler deliberately reads the raw body rather than a typed Pydantic model so it can patch 'hidden' and 'pinned_models' independently, which means shape validation is manual: any JSON array, string, number, null, or bare true/false as the body triggers this.
Source
Thrown at routes/model_routes.py:2379
@router.patch("/model-endpoints/{ep_id}/models")
async def update_hidden_models(ep_id: str, request: Request):
"""Bulk update hidden and/or pinned model lists for an endpoint.
Expects JSON body with optional keys:
{"hidden": ["model-id-1", ...], "pinned_models": ["deploy-id", ...]}
Each key is updated only when present, so callers can patch one list
without clobbering the other.
"""
require_admin(request)
db = SessionLocal()
try:
ep = db.query(ModelEndpoint).filter(ModelEndpoint.id == ep_id).first()
if not ep:
raise HTTPException(404, "Endpoint not found")
body = await request.json()
if not isinstance(body, dict):
raise HTTPException(400, "Body must be a JSON object")
if "hidden" in body:
hidden = body.get("hidden")
if not isinstance(hidden, list):
raise HTTPException(400, "hidden must be a list of model IDs")
base = _normalize_base(ep.base_url)
kind = _effective_endpoint_kind(ep, base)
if _picker_requires_pinning(base, kind):
# Compatibility for older/admin UI paths that still submit
# the previous hide-list shape. API pickers are allow-lists:
# convert "unchecked models" into an explicit pinned list so
# Settings summary, /api/models, and chat agree.
selected = _visible_models(_cached_model_ids(ep), hidden, None)
ep.pinned_models = json.dumps(selected)
ep.hidden_models = None
else:
ep.hidden_models = json.dumps(hidden) if hidden else None
# Accept either "pinned" or "pinned_models" for the manual IDs list.
if "pinned_models" in body or "pinned" in body:View on GitHub (pinned to f9235ebbf1)
Solutions
- Send an object with the documented keys: {"hidden": [...]} and/or {"pinned_models": [...]}; omit a key to leave that list untouched.
- If you only have the array, wrap it client-side before sending.
- Check for double-encoding: JSON.stringify once, not twice.
- Verify Content-Type: application/json so the body parses as JSON at all.
Example fix
# before (wrong shape)
await client.patch(f"/model-endpoints/{ep}/models-visibility", json=["gpt-4o"])
# after
await client.patch(f"/model-endpoints/{ep}/models-visibility", json={"hidden": ["gpt-4o"]}) Defensive patterns
Strategy: type-guard
Validate before calling
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
throw new TypeError('visibility payload must be a JSON object');
} Type guard
function isVisibilityBody(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
} Try / catch
try { await api.patchVisibility(epId, body); }
catch (e) { if (e.status === 400) console.error('wrap arrays: {"hidden": [...]}, not bare arrays'); throw e; } Prevention
- Always wrap list values in an object with the documented key names.
- JSON.stringify exactly once; check for double-encoded bodies in proxy logs.
- Keep integration tests that assert the exact body shape.
When it happens
Trigger: POSTing `["model-a","model-b"]` directly instead of `{"hidden": [...]}`. Sending a bare JSON string/number, or 'null'. A client double-encoding: body is a JSON-encoded string containing JSON. Note malformed JSON raises earlier (json.JSONDecodeError -> 500/422 path), so this 400 specifically means valid-JSON-wrong-shape.
Common situations: Frontend refactors that send the array directly because 'hidden' is the only field. curl tests with `--data '[...]'` instead of `--data '{"hidden":[...]}'`. A proxy or serializer wrapping/rewriting the payload.
Related errors
- hidden must be a list of model IDs
- Current password is incorrect
- {key} must be an integer
- Password is required
- Invalid recurring occurrence uid
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/a6e950b903dbeb95.
Report an issue: GitHub.