"[object Object]" is not valid JSON: Every Cause and Fix

Published 23 Sep 2026 · 7 min read

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.

Quick answer: you're calling 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.

SymptomMost likely causeFix
Fails right after an HTTP call, on the response bodyThe HTTP client (axios, ky, wretch) already parsed the JSON responseUse the response object directly — drop the extra JSON.parse()
Fails inside an Express route handlerexpress.json()/body-parser already parsed req.bodyRead req.body directly; parsing middleware already did this
Fails reading a value back out of localStorageThe object was stored via setItem() without JSON.stringify() firstJSON.stringify() before setItem(), JSON.parse() after getItem() — every time
Fails inside a Web Worker or window message handlerpostMessage already structured-clones objects; event.data is realUse event.data directly — postMessage was never JSON to begin with
Message says Unexpected token 'o' instead of quoting the whole stringSame double-parse mistake, but the re-parsed value is an array of 2+ objectsSame 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:

Open this example in JSON Lab

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:

Open this example in JSON Lab

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:

Open this example in JSON Lab

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 Lab

5. 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:

Open this example in JSON Lab

Debugging checklist

  1. Before touching the JSON.parse() call, run typeof value on whatever you're passing it. If it says "object", not "string", you've found the bug — the value never needed parsing.
  2. 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.
  3. If the value came from storage (localStorage, a database column, a cache), check the code that wrote it — a missing JSON.stringify() on the write side produces exactly this failure on the read side, later and in a different function.
  4. 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.
  5. Fix it by removing the redundant JSON.parse() call, not by wrapping it in try/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 →

← Back to Blog · See also: Unexpected Token in JSON →