{"record":{"id":"9255d8a12754974b","repo":"typicode/json-server","slug":"body-must-be-a-json-object","errorCode":null,"errorMessage":"Body must be a JSON object","messagePattern":"Body must be a JSON object","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"src/app.ts","lineNumber":72,"sourceCode":"  const pageRaw = params.get('_page')\n  const perPageRaw = params.get('_per_page')\n  const page = pageRaw === null ? undefined : Number.parseInt(pageRaw, 10)\n  const perPage = perPageRaw === null ? undefined : Number.parseInt(perPageRaw, 10)\n\n  return {\n    where,\n    sort: params.get('_sort') ?? undefined,\n    page: Number.isNaN(page) ? undefined : page,\n    perPage: Number.isNaN(perPage) ? undefined : perPage,\n    embed: req.query['_embed'],\n  }\n}\n\nfunction withBody(action: (name: string, body: Record<string, unknown>) => Promise<unknown>) {\n  return async (req: any, res: any, next: any) => {\n    const { name = '' } = req.params\n    if (!isItem(req.body)) {\n      res.status(400).json({ error: 'Body must be a JSON object' })\n      return\n    }\n    res.locals['data'] = await action(name, req.body)\n    next?.()\n  }\n}\n\nfunction withIdAndBody(\n  action: (name: string, id: string, body: Record<string, unknown>) => Promise<unknown>,\n) {\n  return async (req: any, res: any, next: any) => {\n    const { name = '', id = '' } = req.params\n    if (!isItem(req.body)) {\n      res.status(400).json({ error: 'Body must be a JSON object' })\n      return\n    }\n    res.locals['data'] = await action(name, id, req.body)\n    next?.()","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/typicode/json-server/blob/89a34a44b7a6a5311dc84f3b8a1b8b45c0905aea/src/app.ts#L54-L90","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ncurl -X POST http://localhost:3000/posts -d '{\"title\":\"hi\"}'\n// -> 400 {\"error\":\"Body must be a JSON object\"}\n\n// after\ncurl -X POST http://localhost:3000/posts \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"title\":\"hi\"}'","handlingStrategy":"validation","validationCode":"// before sending: assert a plain object and set the JSON content type\nfunction isValidRequestBody(body: unknown): boolean {\n  return typeof body === 'object' && body !== null && !Array.isArray(body)\n}\n\nawait fetch('/posts', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify(isValidRequestBody(payload) ? payload : { value: payload }),\n})","typeGuard":"function isJsonObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v)\n}","tryCatchPattern":"// This is an HTTP 400 response, not a thrown exception: check the status\nconst res = await fetch('/posts', opts)\nif (res.status === 400) {\n  const { error } = await res.json()\n  if (error === 'Body must be a JSON object') {\n    // fix payload shape/content-type, then resend once\n  }\n}","preventionTips":["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)"],"tags":["http-400","request-validation","json-body","middleware"],"backgroundTag":"json-body-validation-failed","analyzedSha":"89a34a44b7a6a5311dc84f3b8a1b8b45c0905aea","analyzedAt":"2026-08-25T10:20:06.751Z","schemaVersion":2},"datasetVersion":"2026-08-25T11:17:15.655Z"}