Parsing JSON in JavaScript

1

Parsing JSON in JavaScript

▶

When JavaScript applications communicate with servers, read from files, or consume APIs, data almost always arrives as a JSON string — a sequence of plain text characters that looks like a JavaScript object but is not one. Before your code can meaningfully work with that data — reading properties, iterating arrays, performing calculations — the string must be converted into a live JavaScript value. That conversion is called JSON parsing, and JavaScript provides a built-in tool for it: JSON.parse().

Understanding parsing deeply means understanding not just the function call itself, but the rules JSON imposes on its text format, the ways parsing can fail, the full range of data structures it can produce, and the advanced customization available through the optional reviver function. Each of these dimensions is explored in detail below.

What Is JSON Parsing?

JSON (JavaScript Object Notation) is a text-based data format. When it travels across a network or sits in a file, it is just a string — a flat sequence of characters. Consider this example:

const jsonString = '{"name":"Alice","age":30}';

At this point, jsonString is nothing more than a JavaScript string primitive. Its contents resemble an object literal, but JavaScript does not treat it as one. If you try to access a property directly on the raw string, the result is not what you expect:

console.log(jsonString.name);  // undefined — strings don't have a .name property
console.log(jsonString.age);   // undefined

The text inside the string is meaningless to the JavaScript engine until it is parsed — read character by character, validated against JSON's grammar rules, and converted into a real JavaScript value with actual properties and prototype chain behaviour. Once parsing is complete, you can use dot notation or bracket notation exactly as you would with any ordinary object:

const parsed = JSON.parse(jsonString);

console.log(parsed.name);       // "Alice"
console.log(parsed["age"]);     // 30
console.log(typeof parsed);     // "object"

The distinction matters enormously in practice. API responses fetched with fetch(), data read from localStorage, and payloads received via WebSockets all begin life as strings. Attempting to treat them as structured objects without parsing first is one of the most common sources of subtle bugs in JavaScript applications.

Using JSON.parse()

JSON.parse() is a static method on the built-in JSON object. In its simplest form it accepts a single argument — the JSON string to be parsed — and returns the corresponding JavaScript value:

const result = JSON.parse(text);

The return value is not always an object. The JSON specification allows any of its supported value types at the top level, and JSON.parse() faithfully returns whichever JavaScript type corresponds:

JSON text passed in JavaScript value returned typeof result
'{"key":"val"}' An object with property key "object"
'[1,2,3]' An array [1, 2, 3] "object"
'"hello"' The string "hello" "string"
'42' The number 42 "number"
'true' The boolean true "boolean"
'null' The value null "object"

A particularly powerful aspect of JSON.parse() is that it handles deeply nested structures in a single call. There is no need to recursively parse inner objects or arrays — the entire tree is converted at once:

const json = '{"user":{"name":"Bob","scores":[95,87,100],"address":{"city":"Austin"}}}';
const data = JSON.parse(json);

console.log(data.user.name);              // "Bob"
console.log(data.user.scores[1]);         // 87
console.log(data.user.address.city);      // "Austin"

Every level of nesting — objects within objects, arrays within objects, objects within arrays — is fully materialised and immediately accessible after the single JSON.parse() call returns.

Valid JSON Requirements Before Parsing

JSON is not JavaScript. Although it was inspired by JavaScript's object literal syntax, JSON is a strict, independent format with its own grammar. Many things that are perfectly legal in a JavaScript object literal are invalid in JSON and will cause JSON.parse() to throw a SyntaxError. Understanding these rules before parsing saves significant debugging time.

  • Double quotes only. Both object keys and string values must be enclosed in double quotes. Single quotes are not permitted anywhere in JSON. '{"name":"Alice"}' is valid; "{'name':'Alice'}" is not.
  • No trailing commas. A trailing comma after the last property in an object or the last element in an array is illegal in JSON, even though modern JavaScript allows it in object and array literals. '{"a":1,"b":2,}' will throw.
  • No comments. JSON does not support // or /* */ comments. Adding them makes the string invalid.
  • Restricted value types. Only strings, numbers, objects, arrays, true, false, and null are valid JSON values. JavaScript-specific types like undefined, functions, Symbol, BigInt, Date objects, and RegExp objects are not expressible in JSON. A Date, for instance, is typically serialised as a string ("2024-01-15T10:30:00.000Z") and must be explicitly converted back to a Date object after parsing.
  • Numbers follow specific rules. JSON numbers cannot have leading zeros (except the zero before a decimal point), cannot be NaN, and cannot be Infinity.

Here is a side-by-side comparison to make these rules concrete:

Text Valid JSON? Reason if invalid
{"name":"Alice"} Yes —
{'name':'Alice'} No Single quotes used
{"name":"Alice","age":30,} No Trailing comma
{"active":true,"data":null} Yes —
{"fn":function(){}} No Functions not allowed
{"ratio":NaN} No NaN not a valid JSON value
[1,2,3] Yes —

Handling Errors with try...catch

When JSON.parse() receives a string that violates JSON's grammar, it does not return null or an empty object — it throws a SyntaxError immediately. If that exception is unhandled, it will propagate up the call stack and, in a browser, may halt execution of your entire script. In a Node.js server it can crash the process. Robust applications must therefore always wrap JSON.parse() calls in a try...catch block when parsing data that comes from an external or untrusted source:

function safeParse(jsonString) {
  try {
    const result = JSON.parse(jsonString);
    return result;
  } catch (error) {
    console.error("Failed to parse JSON:", error.message);
    return null;  // or a sensible default value
  }
}

console.log(safeParse('{"name":"Alice"}'));   // { name: 'Alice' }
console.log(safeParse('{bad json}'));         // logs error, returns null

Inside the catch block, the error object is always a SyntaxError instance when thrown by JSON.parse(). Its message property contains a description of exactly what went wrong, which is useful for logging. You have several options for what to do in the catch block:

  • Return a default value (such as null, {}, or []) so the rest of the application can continue with a known safe state.
  • Show a user-facing message informing the user that data could not be loaded.
  • Re-throw a more descriptive error if you want calling code further up the stack to handle it.
  • Log to a monitoring service so that unexpected bad input in production is captured and investigated.

A common real-world pattern when working with the Fetch API illustrates why error handling is critical. The response.json() method also parses JSON under the hood and returns a promise that rejects on malformed input:

async function loadUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const user = await response.json();  // internally calls JSON.parse()
    return user;
  } catch (error) {
    console.error("Could not load user:", error.message);
    return null;
  }
}

Beyond wrapping the call, it is good practice to validate or sanitise external data before passing it to JSON.parse(). For instance, checking that a value is a non-empty string before attempting to parse it avoids a whole class of unnecessary errors:

function parseIfString(value) {
  if (typeof value !== "string" || value.trim() === "") {
    return null;
  }
  try {
    return JSON.parse(value);
  } catch {
    return null;
  }
}

Parsing JSON Arrays

Not every JSON payload represents an object. When a server returns a list of records — search results, product catalogues, user notifications — the top-level JSON value is typically an array. JSON.parse() handles this seamlessly: if the JSON text begins with [, the return value is a JavaScript array.

const jsonArray = '[{"id":1,"title":"Learn JSON"},{"id":2,"title":"Master Fetch"}]';
const courses = JSON.parse(jsonArray);

console.log(Array.isArray(courses));  // true
console.log(courses.length);          // 2
console.log(courses[0].title);        // "Learn JSON"

Array elements can be of any valid JSON type, and they are all fully parsed. A JSON array can contain objects, numbers, strings, booleans, null, or even nested arrays, and every element is converted to its JavaScript equivalent in one pass:

const mixed = '[1, "hello", true, null, {"key":"val"}, [10, 20]]';
const arr = JSON.parse(mixed);

console.log(arr[0]);        // 1
console.log(arr[1]);        // "hello"
console.log(arr[2]);        // true
console.log(arr[3]);        // null
console.log(arr[4].key);    // "val"
console.log(arr[5][0]);     // 10

Because the result is a proper JavaScript array, all standard array methods are immediately available. This makes it straightforward to transform, filter, or iterate the parsed data:

const usersJson = '[{"name":"Alice","active":true},{"name":"Bob","active":false},{"name":"Carol","active":true}]';
const users = JSON.parse(usersJson);

// Get only active users
const activeUsers = users.filter(u => u.active);
console.log(activeUsers.map(u => u.name));  // ["Alice", "Carol"]

// Collect all names
users.forEach(u => console.log(u.name));    // Alice, Bob, Carol

Using the Reviver Function

JSON.parse() accepts an optional second argument called the reviver. It is a function that gives you precise control over how each parsed value is transformed before being returned. Its signature is:

JSON.parse(text, function reviver(key, value) {
  // return the value to keep, a modified value to replace it,
  // or undefined to remove the key entirely
});

After the JSON string has been syntactically parsed into an initial object tree, the reviver is called recursively for every key-value pair in the result, starting from the deepest nested values and working outward toward the root. When processing is complete, the reviver is called one final time with an empty string ("") as the key and the entire root value as the value.

Example 1 — Converting date strings back into Date objects:

A common problem is that JSON has no date type; dates are serialised as ISO strings. When parsed naively, they remain strings. The reviver lets you detect these strings and convert them automatically:

const json = '{"user":"Alice","createdAt":"2024-03-15T09:00:00.000Z"}';

const data = JSON.parse(json, (key, value) => {
  // Check if the value looks like an ISO date string
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
    return new Date(value);
  }
  return value;  // return everything else unchanged
});

console.log(data.user);                      // "Alice"
console.log(data.createdAt instanceof Date); // true
console.log(data.createdAt.getFullYear());   // 2024

Example 2 — Removing unwanted keys:

Returning undefined from the reviver instructs JSON.parse() to omit that key entirely from the result. This is useful for stripping sensitive or irrelevant fields from an API response before the data reaches your application logic:

const json = '{"name":"Alice","password":"s3cr3t","age":30}';

const safe = JSON.parse(json, (key, value) => {
  if (key === "password") return undefined;  // remove this key
  return value;
});

console.log(safe);  // { name: 'Alice', age: 30 }
// "password" has been completely removed

Example 3 — Type coercion and normalisation:

APIs sometimes return numbers as strings (a common quirk of certain back-end systems or CSV-to-JSON converters). The reviver can coerce them to proper numbers during the parse phase, so the rest of your code never has to deal with the inconsistency:

const json = '{"productId":"101","price":"29.99","quantity":"5"}';

const order = JSON.parse(json, (key, value) => {
  // If it looks like a numeric string, convert it
  if (typeof value === "string" && !isNaN(value) && value.trim() !== "") {
    return Number(value);
  }
  return value;
});

console.log(order.price + order.quantity);  // 34.99 (numeric addition, not string concat)
console.log(typeof order.productId);        // "number"

The reviver's traversal order — deepest first, root last — means that by the time a parent object is processed, all of its children have already been transformed. This ordering is important to understand if your reviver logic for a parent depends on the final values of its children.

To summarise the full capability of JSON.parse() including its optional second argument:

  • Call JSON.parse(jsonString) to convert any valid JSON text into the corresponding JavaScript value.
  • The return type mirrors the top-level JSON type: object, array, string, number, boolean, or null.
  • Deeply nested structures are fully parsed in a single call — no recursion is needed on your part.
  • Always wrap calls in try...catch when the input comes from an external, user-supplied, or otherwise untrusted source.
  • Ensure the input is well-formed JSON: double-quoted keys and strings, no trailing commas, no comments, no functions or undefined values.
  • Use the optional reviver function to transform, coerce, or filter values at parse time, keeping your downstream code clean and consistent.
NotesHow to convert a JSON string into a usable JavaScript object using JSON.parse(), including handling common errors.