"[object Object]" is not valid JSON: Every Cause and Fix
JSON.parse() only accepts a string. Every cause in this guide is the same underlying mistake wearing a different outfit: something upstream already turned your JSON text into a real JavaScript object, and this JSON.parse() call is parsing an object it was never given a string for in the first place. JavaScript coerces the object argument to a string before parsing it — and Object.prototype.toString() turns any plain object into the literal ten characters [object Object], which isn't valid JSON syntax. Five real places this happens, each with a live, prefilled JSON Lab example so you can watch the exact failure instead of reading a description of one.
JSON.parse() on something that is already an object, not a JSON string. Find whatever produced that value — an HTTP client, a middleware layer, a storage read — and either remove the redundant JSON.parse() call, or fix whatever earlier step failed to call JSON.stringify() before handing the value off as text.
Quick reference
All five results below were verified against real Node.js/V8 output before publishing — the same engine Chrome, Edge, and JSON Lab itself run.
| Symptom | Most likely cause | Fix |
|---|---|---|
| Fails right after an HTTP call, on the response body | The HTTP client (axios, ky, wretch) already parsed the JSON response | Use the response object directly — drop the extra JSON.parse() |
| Fails inside an Express route handler | express.json()/body-parser already parsed req.body | Read req.body directly; parsing middleware already did this |
Fails reading a value back out of localStorage | The object was stored via setItem() without JSON.stringify() first | JSON.stringify() before setItem(), JSON.parse() after getItem() — every time |
| Fails inside a Web Worker or window message handler | postMessage already structured-clones objects; event.data is real | Use event.data directly — postMessage was never JSON to begin with |
Message says Unexpected token 'o' instead of quoting the whole string | Same double-parse mistake, but the re-parsed value is an array of 2+ objects | Same fix — the message just names a character because the stringified array is longer |
1. An HTTP client that already parses JSON for you
This is the most common trigger. Libraries like axios, ky, and wretch inspect the response's Content-Type header and parse a JSON body into a real object automatically — that's the entire point of using them over raw fetch(). Code written against a lower-level client, or copied from a raw-fetch example, often keeps a JSON.parse() call that made sense there but is now parsing an object axios already handed back.
const response = await axios.get('/api/user');
// response.data is ALREADY a plain object — axios parsed it for you
const user = JSON.parse(response.data);
// SyntaxError: "[object Object]" is not valid JSON
The fix is to delete the call, not add a try/catch around it: const user = response.data; — the parsing already happened.
See it fail live
Paste the literal text an object like response.data coerces to, and JSON Lab reports the exact same failure axios's caller would see:
2. Express route handlers re-parsing a body middleware already parsed
express.json() (or the older body-parser) reads the raw request stream, parses it once, and assigns the result to req.body before any route handler runs. A handler that calls JSON.parse(req.body) — often copied from an older Express version, or from a webhook handler that predates adding the middleware — is parsing a value that was never a string in that request's lifecycle.
app.use(express.json());
app.post('/webhook', (req, res) => {
const payload = JSON.parse(req.body); // req.body is already an object here
// SyntaxError: "[object Object]" is not valid JSON
res.sendStatus(200);
});
Use req.body directly. If the route genuinely needs the raw text too (for signature verification on a webhook, for example), configure express.json({ verify }) to capture the raw buffer separately instead of trying to re-derive it from the already-parsed object.
See it fail live
Same underlying value, same failure — this is what req.body looks like once String() gets applied to it a second time:
3. localStorage written without JSON.stringify() first
This one is different from the first two — the bug is upstream, in whatever wrote the value, not in the JSON.parse() call itself. localStorage.setItem() coerces whatever you pass it to a string. Pass it a plain object instead of a pre-stringified one, and it silently stores the literal text [object Object] — no error, no warning, just the wrong ten characters saved to disk. The failure doesn't surface until something later tries to read that key back as JSON.
const currentUser = { id: 42, name: 'Ava' };
localStorage.setItem('user', currentUser); // forgot JSON.stringify(currentUser)
// storage now genuinely contains the string "[object Object]"
// ...later, possibly in a different session:
const user = JSON.parse(localStorage.getItem('user'));
// SyntaxError: "[object Object]" is not valid JSON
Fix the write, not just the read: localStorage.setItem('user', JSON.stringify(currentUser)). The read-side JSON.parse() was correct all along — it just never had valid JSON to parse, because nothing ever put any there.
See it fail live
This is exactly what a getItem() call returns once a write like the one above has already happened — a real string, ten characters, that was never JSON:
4. Worker and window message handlers
postMessage() — from a Web Worker, an iframe, or another browser window — never serializes to JSON at all. It uses the structured clone algorithm, which can already represent objects, arrays, Maps, Dates, and more directly. event.data in the receiving handler is a real, already-reconstructed object, not text. Code that treats every incoming message as a string to be parsed — often defensive code copied from a different transport that genuinely did send JSON — fails the moment a caller sends a real object instead of a JSON string.
worker.onmessage = (event) => {
const data = JSON.parse(event.data); // event.data is already structured-cloned
// SyntaxError: "[object Object]" is not valid JSON
};
// meanwhile, in the worker:
self.postMessage({ status: 'done', result: 42 }); // sent as a real object, not JSON text
Use event.data directly. If the two sides need to agree on a stable wire format instead of relying on structured clone's engine-specific behavior, stringify deliberately on the sending side and parse deliberately on the receiving side — don't parse only sometimes.
See it fail live
Same failure, same cause — a message handler parsing something the sender never stringified:
Open this example in JSON Lab5. A plain double-parse, and why the message changes for arrays
The simplest version of this bug has no framework involved at all: a value gets parsed once, correctly, and then handed to a second JSON.parse() call somewhere further down a pipeline — a logging wrapper, a caching layer, a generic "parse if needed" helper — that doesn't check whether the work is already done.
const data = JSON.parse(rawBody); // correct — rawBody was a real JSON string
// ...three files later, in code that doesn't know `data` is already parsed:
const same = JSON.parse(data);
// SyntaxError: "[object Object]" is not valid JSON
A single object and a one-element array of objects both stringify to exactly [object Object], so V8 can quote the whole broken value in the message. An array with more than one object stringifies to [object Object],[object Object] — too long to quote directly — so the engine instead names the specific character where parsing broke:
JSON.parse([{a: 1}, {b: 2}]);
// Unexpected token 'o', "[object Obj"... is not valid JSON
// ^ position 1 — right after the first array element's "[")
It's the identical root cause and the identical fix — stop parsing a value that's already an object — the message just looks different because String() produces a longer, differently-shaped string for a multi-element array than it does for a single object. Guard the generic helper with typeof value === 'string' before parsing, rather than wrapping the call in a blind try/catch.
See it fail live
The single-object version — try it, then paste [object Object],[object Object] yourself to see the token-based message the array variant produces instead:
Debugging checklist
- Before touching the
JSON.parse()call, runtypeof valueon whatever you're passing it. If it says"object", not"string", you've found the bug — the value never needed parsing. - Trace the value back one step at a time: an HTTP client, a middleware layer, and a message handler are the three most common places something already parsed it for you.
- If the value came from storage (
localStorage, a database column, a cache), check the code that wrote it — a missingJSON.stringify()on the write side produces exactly this failure on the read side, later and in a different function. - If the message names a character instead of quoting the whole value, the re-parsed value is very likely an array with more than one element — same bug, same fix.
- Fix it by removing the redundant
JSON.parse()call, not by wrapping it intry/catch— a caught error here still means the rest of the function runs without the data it expected.
Two related JSON failures worth knowing apart from this one: "undefined" is not valid JSON happens when you're missing data that was never written correctly, and unexpected end of JSON input happens when a real JSON string got cut off mid-transfer. This one is the opposite kind of problem — you already have the data, in the right shape, and the only bug is asking JSON.parse() to do something to it a second time.
📋 Paste it and see exactly where it breaks
JSON Lab formats, validates, and repairs JSON entirely in your browser — nothing you paste is uploaded anywhere.
Open JSON Lab →