typicode/json-server · error
Body must be a JSON object
Error message
Body must be a JSON object
What it means
This 400 response comes from the withBody middleware wrapper in src/app.ts:68-78, which guards every collection-scoped write route: POST /:name (create), PUT /:name (update), and PATCH /:name (patch). It passes req.body to isItem() from service.ts, which accepts only a plain JSON object. If the body is an array, a string, a number, null, or missing entirely, the request is rejected before it reaches the Service layer, because the lowdb-backed store keys records by id inside an object and cannot index a non-object body.
Source
Thrown at src/app.ts:72
const pageRaw = params.get('_page')
const perPageRaw = params.get('_per_page')
const page = pageRaw === null ? undefined : Number.parseInt(pageRaw, 10)
const perPage = perPageRaw === null ? undefined : Number.parseInt(perPageRaw, 10)
return {
where,
sort: params.get('_sort') ?? undefined,
page: Number.isNaN(page) ? undefined : page,
perPage: Number.isNaN(perPage) ? undefined : perPage,
embed: req.query['_embed'],
}
}
function withBody(action: (name: string, body: Record<string, unknown>) => Promise<unknown>) {
return async (req: any, res: any, next: any) => {
const { name = '' } = req.params
if (!isItem(req.body)) {
res.status(400).json({ error: 'Body must be a JSON object' })
return
}
res.locals['data'] = await action(name, req.body)
next?.()
}
}
function withIdAndBody(
action: (name: string, id: string, body: Record<string, unknown>) => Promise<unknown>,
) {
return async (req: any, res: any, next: any) => {
const { name = '', id = '' } = req.params
if (!isItem(req.body)) {
res.status(400).json({ error: 'Body must be a JSON object' })
return
}
res.locals['data'] = await action(name, id, req.body)
next?.()View on GitHub (pinned to 89a34a44b7)
Solutions
- Set Content-Type: application/json on the request (curl -H 'Content-Type: application/json' or fetch with headers)
- Send the body as one JSON object, not an array, string, number, or null
- If you need to send many records, POST them one at a time or wrap them in an object such as {"items":[...]}
- Verify no proxy between client and app strips or replaces the request body
Example fix
// before
curl -X POST http://localhost:3000/posts -d '{"title":"hi"}'
// -> 400 {"error":"Body must be a JSON object"}
// after
curl -X POST http://localhost:3000/posts \
-H 'Content-Type: application/json' \
-d '{"title":"hi"}' Defensive patterns
Strategy: validation
Validate before calling
// before sending: assert a plain object and set the JSON content type
function isValidRequestBody(body: unknown): boolean {
return typeof body === 'object' && body !== null && !Array.isArray(body)
}
await fetch('/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(isValidRequestBody(payload) ? payload : { value: payload }),
}) Type guard
function isJsonObject(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v)
} Try / catch
// This is an HTTP 400 response, not a thrown exception: check the status
const res = await fetch('/posts', opts)
if (res.status === 400) {
const { error } = await res.json()
if (error === 'Body must be a JSON object') {
// fix payload shape/content-type, then resend once
}
} Prevention
- Always set Content-Type: application/json on every write request
- Centralize fetch calls in one client wrapper that stringifies objects and sets headers
- Never send a bare array, string, number, or null as the whole body
- In tests, use .set('Content-Type','application/json').send(obj) rather than .send(string)
When it happens
Trigger: POST /posts with body [1,2,3] (a JSON array); PUT /posts with body "text" or 42; POST /posts with no Content-Type: application/json header, so the milliparsec json() middleware (app.use(json()) at src/app.ts:119) skips parsing and req.body stays undefined; POST /posts with an empty body; PATCH /posts with body null.
Common situations: A curl or fetch client omits Content-Type: application/json, so the JSON body parser never runs and req.body is undefined; the client models the resource as a list and sends a JSON array; a test harness sends .send(string) instead of .send(object); a proxy or gateway rewrites the body to form-encoded data, which this app does not parse.
AI-assisted analysis of typicode/json-server@89a34a44b7 (2026-08-25).
Data as JSON: /api/errors/9255d8a12754974b.
Report an issue: GitHub.