JSON parse(): Reviver, Errors and Examples

Learn JSON parse in JavaScript: the reviver function, parsing dates, safe error handling, real SyntaxError messages and big number precision.

Tree of the JSON text {"a": 1, "b": {"c": 2}, "d": [3]} with numbered call order: a 1, c 2, b 3, array item 0 4, d 5, root 6

The method JSON.parse() is the built-in JavaScript method that reads a string of JSON text and returns the value it describes: an object, an array, a string, a number, a boolean or null. It is the step that turns data received as text, from an API, a file or browser storage, into values our code can use. The method is strict. If the text breaks any JSON rule, it throws a SyntaxError instead of guessing, so every call that reads outside data needs error handling.

This page covers the syntax and return value first, then the errors the method throws and the most likely cause of each one in an API client. The second half explains the optional reviver function, which can turn date strings into dates, drop fields and keep large numbers exact. It ends with two security traps and the questions developers ask most often. The reverse step, turning values into text, is covered on the JSON.stringify() page.

1. Syntax and Return Value of JSON.parse()

The method takes one required argument and one optional argument, as described in the MDN reference.

JSON.parse(text)
JSON.parse(text, reviver)
  • text is the JSON string to read. The whole string must be one valid JSON value, with optional whitespace around it.
  • reviver is an optional function that the parser calls for every value it creates. Section 3 explains it in detail.

The return type depends on the text. An object in the text gives an object, an array gives an array, and a bare number or string gives a number or string. Whitespace and line breaks around the value are ignored. For four sample inputs, Node.js 22 prints these values, and a browser console shows the same values in a slightly different format:

console.log(JSON.parse('{"x": 5, "y": 6}'));
console.log(JSON.parse('["A", "B", "C"]'));
console.log(JSON.parse('42'), JSON.parse('"hello"'), JSON.parse('true'), JSON.parse('null'));
console.log(JSON.parse('  {"x": 5}\n  '));
{ x: 5, y: 6 }
[ 'A', 'B', 'C' ]
42 hello true null
{ x: 5 }

Arrays get a page of their own: JSON arrays shows how to loop over and change the array that JSON.parse() returns.

1.1. Input That Is Not a String

The method JSON.parse() converts its argument to a string before it reads it, so a value that is not a string can give an unexpected result. The call JSON.parse(null) returns null, because the text “null” is valid JSON. The call JSON.parse(123) returns 123 for the same reason. An object becomes the text “[object Object]”, which is not JSON, so the call throws:

try { JSON.parse({ id: 1 }); } catch (e) { console.log(e.name + ': ' + e.message); }
// SyntaxError: "[object Object]" is not valid JSON

The “[object Object]” error almost always means the value was parsed already. A common case is calling JSON.parse() on the result of response.json(), which has done the parsing for us.

2. Errors JSON.parse() Throws and What They Mean

Every failure is a SyntaxError, but the message tells us a lot about the cause. Node.js 22 reports five broken inputs with these messages:

"" -> SyntaxError: Unexpected end of JSON input
"{\"x\": 5" -> SyntaxError: Expected ',' or '}' after property value in JSON at position 7 (line 1 column 8)
"{\"x\": 5} {\"y\": 6}" -> SyntaxError: Unexpected non-whitespace character after JSON at position 9 (line 1 column 10)
"<!DOCTYPE html><html>" -> SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
"undefined" -> SyntaxError: "undefined" is not valid JSON

The position in the message counts characters from 0, while line and column count from 1. In an API client, most of these messages come from the HTTP response, such as an empty body or an HTML error page, and not from a typo in the JSON.

Message begins withUsual causeWhat to check
Unexpected end of JSON inputAn empty body, such as an HTTP 204 response, or text that was cut offThe status code and the body length before parsing
Expected ‘,’ or ‘}’ after property valueText cut off in the middle, or a missing commaWhether the full body arrived
Unexpected non-whitespace character after JSONTwo JSON documents in one string, such as a JSON Lines file read as a wholeSplit the text by line and parse each line
Unexpected token ‘<‘The server sent an HTML page, often an error page or a login redirectThe status code and the Content-Type header
“undefined” is not valid JSONThe variable passed in was undefinedWhere the value came from

Syntax rules themselves, such as double quotes and no trailing commas, are explained on the JSON syntax page.

2.1. Wrap Parsing in try…catch

Text from outside the program can always be broken, so the parse call belongs inside try…catch. A small helper that returns either the data or the error keeps the rest of the code clean:

function parseJson(text) {
  try {
    return { ok: true, data: JSON.parse(text) };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}
console.log(parseJson('{"status": "active"}'));
console.log(parseJson('{status: "active"}'));
{ ok: true, data: { status: 'active' } }
{
  ok: false,
  error: "Expected property name or '}' in JSON at position 1 (line 1 column 2)"
}

Keep the try block small. If it also wraps the code that uses the data, a TypeError in that code is reported as a parse error, and we search for the bug in the wrong place.

2.2. Check the Response Before Parsing It

With fetch(), the method response.json() reads the body and parses it as JSON in one step. It fails with the same SyntaxError when the server returns HTML or an empty body. Checking the status and the Content-Type header first turns a confusing parse error into a clear message:

const response = await fetch('https://api.example.com/orders/7');
if (!response.ok) {
  throw new Error('HTTP ' + response.status);
}
const type = response.headers.get('content-type') || '';
if (!type.includes('application/json')) {
  throw new Error('Expected JSON but got ' + type);
}
const order = await response.json();

The JSON with Ajax page covers fetch() requests in more depth.

3. Transforming Values with a Reviver

A reviver is a function passed as the second argument. After the text is parsed, JSON.parse() calls it once for every key and value, and whatever the function returns replaces the original value. Returning undefined removes the property. The reviver gets the key as a string, the parsed value and, in current engines, a third context argument.

The calls happen from the inside out. The parser visits the deepest values first and calls the reviver for a parent only after all its children are done. The last call is for the whole result, with an empty string as the key. Logging each key inside the reviver shows the order:

const seen = [];
JSON.parse('{"a": 1, "b": {"c": 2}, "d": [3]}', function (key, value) {
  seen.push(JSON.stringify(key));
  return value;
});
console.log(seen.join(' '));
"a" "c" "b" "0" "d" ""
Tree of the JSON text {
The reviver visits the deepest values first and the root last, so a parent is always called after its children.

Two rules follow from this order. A reviver must return value for every key it does not change, or those values disappear. And if it returns undefined for the empty key, the whole result becomes undefined.

3.1. Turn Date Strings into Date Objects

JSON has no date type, so APIs send dates as text, most often in ISO 8601 format. After a plain parse, createdAt is only a string. A reviver can turn matching strings into Date objects. The pattern below is strict on purpose, so ordinary strings that contain a date-like part stay strings.

const text = '{"id": 7, "createdAt": "2026-10-02T10:15:00.000Z", "tags": ["a"]}';
const plain = JSON.parse(text);
console.log(typeof plain.createdAt);

const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
const order = JSON.parse(text, (key, value) =>
  typeof value === 'string' && ISO.test(value) ? new Date(value) : value);
console.log(order.createdAt instanceof Date, order.createdAt.getUTCFullYear());
string
true 2026

In a larger app, converting by field name, such as every key that ends in At, is often safer than matching every string. Our article on dates, times and money explains date formats for APIs.

3.2. Drop a Field While Parsing

Returning undefined deletes a property. In this example, the reviver removes the password before the object reaches the rest of the code:

const user = JSON.parse('{"name": "Lokesh", "password": "secret", "role": "admin"}',
  (key, value) => (key === 'password' ? undefined : value));
console.log(user);
{ name: 'Lokesh', role: 'admin' }

Dropping a field in the reviver is fine for tidying data, but it is not a security control. The password was still sent over the network and held in memory as text. The real fix is a server that never sends it.

3.3. Change Numbers Without Breaking Objects

A common version of this example doubles every even number with a check like value % 2 == 0. That check also runs on objects, where value % 2 is NaN, so objects come back unchanged only by luck. Testing the type first makes the intent clear:

const obj = JSON.parse('{"1": 1, "2": 2, "3": {"4": 4, "5": {"6": 6}}}', (key, value) =>
  typeof value === 'number' && value % 2 === 0 ? value * 2 : value);
console.log(JSON.stringify(obj));
{"1":1,"2":4,"3":{"4":8,"5":{"6":12}}}

3.4. Keep Large Integers Exact with context.source

A JavaScript number stores integers without rounding only up to 9007199254740991. A larger id in the text is rounded during parsing, before any reviver sees it. The context argument fixes this. For numbers, strings, booleans and null, context.source holds the original text of the value, which we can pass to BigInt():

const text = '{"orderId": 9007199254740993, "total": 12.5}';
console.log(JSON.parse(text).orderId);

const exact = JSON.parse(text, (key, value, context) =>
  key === 'orderId' ? BigInt(context.source) : value);
console.log(exact.orderId);
console.log(typeof exact.orderId);
9007199254740992
9007199254740993n
bigint

The plain parse turned …993 into …992 without any warning. According to MDN compatibility data, the context argument works in Chrome 114, Firefox 135, Safari 18.4 and Node.js 21 or later. The TC39 proposal behind it reached stage 4 in November 2025 and is part of ECMAScript 2026. For older clients, the portable fix is for the API to send large ids as strings.

4. Two Security Traps After Parsing

The method JSON.parse() only builds data and never runs code. That makes it the safe replacement for the old habit of calling eval() on JSON. Still, two details can cause security bugs once the data is in memory.

4.1. A __proto__ Key Can Pollute a Merge

When the text contains a key named __proto__, JSON.parse() creates an ordinary property with that name. The parsed object itself is harmless. The danger comes when code copies it with Object.assign(), which treats __proto__ as a request to change the prototype of the target:

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
console.log(Object.keys(payload));
console.log(payload.isAdmin);
console.log(Object.assign({}, payload).isAdmin);
console.log({ ...payload }.isAdmin);
[ '__proto__' ]
undefined
true
undefined

The copy made with Object.assign() suddenly reports isAdmin as true, while the spread copy does not. Deep-merge helpers that walk keys by hand have the same problem. Validate request bodies against a JSON Schema, or skip the keys __proto__, constructor and prototype when merging.

4.2. Duplicate Keys Keep the Last Value

If a name appears twice in one object, JSON.parse() keeps the last value without any error. For example, the call JSON.parse(‘{“a”: 1, “a”: 2}’) returns { a: 2 }. Other parsers may keep the first value or reject the text, so two services can read the same document differently. The page on valid JSON key names covers this in more detail.

5. JSON.parse() Questions Developers Ask

These two questions come up in almost every codebase that reads JSON.

5.1. Why Does JSON.parse() Return a String Instead of an Object?

The data was most likely encoded twice. When an object goes through JSON.stringify() two times, the result is a JSON string that contains JSON text, and one parse only removes the outer layer:

const twice = JSON.stringify(JSON.stringify({ id: 1 }));
console.log(twice);
const once = JSON.parse(twice);
console.log(typeof once, once);
console.log(JSON.parse(once));
"{\"id\":1}"
string {"id":1}
{ id: 1 }

Parsing twice works around the symptom. The better fix is to find the code that encodes the data a second time. Often it is a server framework that serializes a value which was already a string.

5.2. What Is the Difference Between JSON.parse() and response.json()?

The method response.json() belongs to the Fetch API. It reads the whole response body and parses it as JSON, and it returns a promise for the result (MDN). The method JSON.parse() works on a string we already have, such as a value from localStorage or a file. Use one or the other on the same data, never both.

6. Conclusion

The method JSON.parse() turns JSON text into JavaScript values and refuses anything that is not valid JSON. Wrap it in a small try…catch, check the status and Content-Type before parsing a response, and read the error message, because it often names the cause. Use a reviver for dates and other conversions, and context.source when ids exceed the safe integer range. Before merging parsed data into other objects, guard against the __proto__ key.

7. References

The specification and documentation pages below describe the exact behavior shown on this page.

Happy Learning !!

Comments

Subscribe
Notify of
0 Comments
Most Voted
Newest Oldest