JSON.parse and JSON.stringify in JavaScript

JSON.parse turns JSON text into a JavaScript value, and JSON.stringify turns a value back into text. Most errors come from text that looks like JSON but breaks one of its strict rules.

JSON.parse(text) takes a string of JSON and returns the JavaScript value it describes, usually an object or an array. JSON.stringify(value) does the reverse and returns a string. If the text breaks a JSON rule, JSON.parse throws a SyntaxError, so wrap it in try/catch.

const text = '{"name": "Ann", "age": 31}';
const user = JSON.parse(text);   // an object
user.name;                         // "Ann"
JSON.stringify(user);              // '{"name":"Ann","age":31}'

Try it with your own text. Paste any JSON below. Valid input comes back indented; broken input shows the error message and marks the character where parsing stopped.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JSON validator</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  textarea, pre {
    box-sizing: border-box; width: 100%; margin: 0;
    font: 13px/1.45 ui-monospace, Consolas, monospace;
    border: 1px solid #d5d9e0; border-radius: 8px; padding: 10px; background: #fff;
  }
  textarea { height: 140px; resize: vertical; }
  pre { min-height: 150px; max-height: 220px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
  .bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 10px 0; }
  button { font: inherit; padding: 7px 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  select { font: inherit; padding: 6px; border-radius: 8px; border: 1px solid #d5d9e0; }
  #status { font-weight: 600; }
  .ok { color: #0f5132; } .bad { color: #9a3412; }
  mark { background: #fde2da; color: #9a3412; outline: 2px solid #f97316; }
</style>
</head>
<body>
<textarea id="input" spellcheck="false">{"name": "Desk lamp", "price": 39.5, "tags": ["light", "office"], "inStock": true,}</textarea>
<div class="bar">
  <button id="check">Parse</button>
  <label>Indent <select id="indent">
    <option value="2">2 spaces</option><option value="4">4 spaces</option><option value="tab">Tab</option>
  </select></label>
  <span id="status"></span>
</div>
<pre id="output"></pre>

<script>
  const input = document.getElementById('input');
  const output = document.getElementById('output');
  const status = document.getElementById('status');
  const indent = document.getElementById('indent');

  // Chrome says "at position 7", Firefox says "line 1 column 8". Turn either into an index.
  function errorIndex(message, text) {
    const pos = message.match(/position (\d+)/);
    if (pos) return Number(pos[1]);
    const lc = message.match(/line (\d+) column (\d+)/);
    if (!lc) return -1;
    const before = text.split('\n').slice(0, lc[1] - 1);
    return before.reduce((n, line) => n + line.length + 1, 0) + Number(lc[2]) - 1;
  }

  function check() {
    const text = input.value;
    output.textContent = '';
    try {
      const data = JSON.parse(text);
      const space = indent.value === 'tab' ? '\t' : Number(indent.value);
      output.textContent = JSON.stringify(data, null, space);
      status.className = 'ok';
      status.textContent = 'Valid JSON (' + (Array.isArray(data) ? 'array' : typeof data) + ')';
    } catch (err) {
      status.className = 'bad';
      status.textContent = err.name + ': ' + err.message;
      const i = errorIndex(err.message, text);
      if (i < 0) { output.textContent = 'This message gives no position.'; return; }
      // Show the text around the error, with the bad character marked
      const mark = document.createElement('mark');
      mark.textContent = text[i] || ' ';
      output.append('Position ' + i + ':\n', text.slice(Math.max(0, i - 40), i), mark, text.slice(i + 1, i + 40));
    }
  }

  document.getElementById('check').addEventListener('click', check);
  indent.addEventListener('change', check);
  check();
</script>
</body>
</html>
A JSON validator and pretty-printer. The sample has a trailing comma; delete it and press Parse.

Text in, object out

JSON is a text format. Whatever hands it to you, a file, a network response, a form field, a block in the page, hands you a string. You cannot read data.name from a string. JSON.parse turns it into a value you can use.

JSON.parse turns text into a value. JSON.stringify turns the value back into text.
JSON.parse turns text into a value. JSON.stringify turns the value back into text.

The result can be any JSON value, not only an object. JSON.parse('[1,2]') returns an array, JSON.parse('42') returns a number, and JSON.parse('"hi"') returns a string.

Parse once, as soon as the text arrives. Then work with the object, and only stringify when it has to leave the page as text again.

Catch bad JSON with try/catch

JSON.parse does not return null or false on bad input. It throws. An uncaught throw stops the rest of the script, so anything that comes from a user or another system should be parsed inside try:

function safeParse(text) {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

Keep the catch for showing the problem, as the validator above does. Swallowing the error and carrying on with an empty object only moves the bug somewhere harder to find.

JSON is stricter than JavaScript

JSON looks like a JavaScript object literal, which is where most errors start. Code that is fine in a .js file is often not valid JSON.

The same data as a JavaScript literal and as valid JSON.
The same data as a JavaScript literal and as valid JSON.

The rules that catch people:

  1. Keys must be in double quotes: "name", not name or 'name'.
  2. Strings use double quotes only.
  3. No trailing comma after the last item in an object or array.
  4. No comments, neither // nor /* */.
  5. Values are strings, numbers, true, false, null, objects and arrays. NaN, Infinity and undefined are not JSON.

Each button below loads one of these mistakes and shows the error this browser gives for it:

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Common JSON mistakes</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
  button { font: inherit; font-size: 14px; padding: 6px 11px; border: 1px solid #c7ccd6; border-radius: 99px; background: #fff; color: #1d2330; cursor: pointer; }
  button[aria-pressed="true"] { background: #1d4ed8; border-color: #1d4ed8; color: #fff; }
  table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 10px; overflow: hidden; }
  th, td { text-align: left; vertical-align: top; padding: 9px 10px; border-bottom: 1px solid #e5e7eb; font-size: 14px; }
  th { width: 70px; color: #6b7280; font-weight: 600; }
  code { font: 13px/1.45 ui-monospace, Consolas, monospace; white-space: pre-wrap; word-break: break-word; }
  .bad { color: #9a3412; } .ok { color: #0f5132; }
</style>
</head>
<body>
<div class="buttons" id="buttons"></div>
<table>
  <tr><th>Text</th><td><code id="broken"></code></td></tr>
  <tr><th>Error</th><td><code id="error" class="bad"></code></td></tr>
  <tr><th>Fixed</th><td><code id="fixed"></code></td></tr>
  <tr><th>Result</th><td><code id="result" class="ok"></code></td></tr>
</table>

<script>
  // Each sample: the broken input and a corrected version
  const samples = {
    'Trailing comma': ['{"a": 1, "b": 2,}', '{"a": 1, "b": 2}'],
    'Single quotes': ["{'name': 'Ann'}", '{"name": "Ann"}'],
    'Unquoted key': ['{name: "Ann"}', '{"name": "Ann"}'],
    'Comment': ['{"a": 1 // one\n}', '{"a": 1}'],
    'NaN': ['{"score": NaN}', '{"score": null}'],
    'undefined': [undefined, '{}'],
    'Empty string': ['', '{}'],
  };

  function tryParse(text) {
    try { return { value: JSON.parse(text) }; }
    catch (err) { return { error: err.name + ': ' + err.message }; }
  }

  function show(name) {
    const [broken, fixed] = samples[name];
    const bad = tryParse(broken);
    const good = tryParse(fixed);
    document.getElementById('broken').textContent =
      broken === undefined ? '(the value undefined)' : (broken || '(empty string)');
    document.getElementById('error').textContent = bad.error || 'No error';
    document.getElementById('fixed').textContent = fixed;
    document.getElementById('result').textContent = good.error || JSON.stringify(good.value);
    for (const b of document.querySelectorAll('#buttons button')) {
      b.setAttribute('aria-pressed', b.textContent === name);
    }
  }

  for (const name of Object.keys(samples)) {
    const b = document.createElement('button');
    b.textContent = name;
    b.addEventListener('click', () => show(name));
    document.getElementById('buttons').append(b);
  }
  show('Trailing comma');
</script>
</body>
</html>
Common mistakes, the error each one throws in your browser, and the fixed text.

The wording differs between browsers. Chrome writes, for a trailing comma in an object: Expected double-quoted property name in JSON at position 7 (line 1 column 8). Firefox reports the same problem in its own words. Trust the position more than the wording.

Mistake Example Chrome wording
Trailing comma in an object {"a":1,} Expected double-quoted property name in JSON at position 7
Trailing comma in an array [1,2,] Unexpected token ']' ... is not valid JSON
Single quotes or bare key {'a':1} Expected property name or '}' in JSON at position 1
Comment {"a":1 // x} Expected ',' or '}' after property value in JSON at position 7
NaN as a value {"a":NaN} Unexpected token 'N' ... is not valid JSON
Empty string '' Unexpected end of JSON input

Turn values back while parsing: the reviver

JSON.parse takes an optional second argument, a reviver function. It is called for every key and value, from the innermost values outward, and whatever it returns replaces the value. Returning undefined removes the key.

The common use is dates. JSON has no date type, so dates travel as strings like "2026-03-14". A reviver can turn them back into Date objects:

const data = JSON.parse(text, (key, value) =>
  typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)
    ? new Date(value)
    : value
);

Keep the test narrow. A pattern that matches any string with digits would turn product codes into dates too.

JSON.stringify: indentation and a replacer

JSON.stringify(value, replacer, space) has two optional arguments. The third, space, makes the output readable. Pass a number of spaces (up to 10) or a string such as '\t':

JSON.stringify({ a: 1, b: [1, 2] }, null, 2);
// {
//   "a": 1,
//   "b": [
//     1,
//     2
//   ]
// }

The second, replacer, filters what gets written. An array of key names keeps only those keys. A function works like a reviver in reverse: return a new value, or undefined to leave the key out.

const user = { name: 'Ann', email: 'ann@example.com', password: 'x' };
JSON.stringify(user, ['name', 'email']);
// '{"name":"Ann","email":"ann@example.com"}'

Not every value survives the trip. stringify quietly drops or converts some of them:

What JSON.stringify writes for values JSON has no type for.
What JSON.stringify writes for values JSON has no type for.

Read JSON from the page, no fetch needed

For data that belongs to one page, put it in the page itself. A <script> element with type="application/json" is not run by the browser. Its content stays as plain text you can read with textContent:

<script type="application/json" id="products">
  [{"name": "Desk lamp", "price": 39.5}]
</script>
<script>
  const products = JSON.parse(document.getElementById('products').textContent);
</script>

This works even when the page is opened straight from the disk, where browsers such as Chrome block fetch from loading a separate .json file. One catch: the text </script> inside the data would end the block early. Write it as <\/script>, which is still valid JSON.

The finished example below reads its rows from such a block, turns the dates into Date objects with a reviver, and sorts by any column. For more on sortable tables, see HTML table with sort and filter.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Table from a JSON block</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  p { margin: 0 0 10px; font-size: 14px; color: #4b5563; }
  table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 10px; overflow: hidden; font-size: 14px; }
  th, td { padding: 9px 10px; border-bottom: 1px solid #e5e7eb; text-align: left; }
  th { background: #eef1f5; white-space: nowrap; }
  th button { all: unset; cursor: pointer; font-weight: 700; }
  th button::after { content: ' \2195'; color: #9aa3b2; }
  th[aria-sort="ascending"] button::after { content: ' \2191'; color: #1d4ed8; }
  th[aria-sort="descending"] button::after { content: ' \2193'; color: #1d4ed8; }
  .num { text-align: right; }
</style>
</head>
<body>
<p>The rows come from the JSON block in this page. Click a heading to sort.</p>
<table>
  <thead><tr>
    <th data-key="name"><button>Product</button></th>
    <th data-key="price" class="num"><button>Price</button></th>
    <th data-key="added"><button>Added</button></th>
  </tr></thead>
  <tbody id="rows"></tbody>
</table>

<!-- The browser does not run this block. It is only text for JSON.parse. -->
<script type="application/json" id="products">
[
  {"name": "USB-C cable <2 m>", "price": 9, "added": "2026-08-02"},
  {"name": "Desk lamp", "price": 39.5, "added": "2026-03-14"},
  {"name": "Monitor stand", "price": 54, "added": "2025-11-20"},
  {"name": "Notebook, dotted", "price": 6.25, "added": "2026-05-09"}
]
</script>

<script>
  // Reviver: turn "YYYY-MM-DD" strings into Date objects while parsing
  const products = JSON.parse(
    document.getElementById('products').textContent,
    (key, value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)
      ? new Date(value + 'T00:00:00') : value
  );

  const tbody = document.getElementById('rows');

  function render() {
    tbody.replaceChildren();
    for (const p of products) {
      const tr = tbody.insertRow();
      // textContent shows "<2 m>" as text; it is never read as HTML
      tr.insertCell().textContent = p.name;
      const price = tr.insertCell();
      price.textContent = '$' + p.price.toFixed(2);
      price.className = 'num';
      tr.insertCell().textContent =
        p.added.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
    }
  }

  for (const th of document.querySelectorAll('th')) {
    th.querySelector('button').addEventListener('click', () => {
      const key = th.dataset.key;
      const dir = th.getAttribute('aria-sort') === 'ascending' ? -1 : 1;
      document.querySelectorAll('th').forEach((h) => h.removeAttribute('aria-sort'));
      th.setAttribute('aria-sort', dir === 1 ? 'ascending' : 'descending');
      // Numbers and Dates subtract; strings use localeCompare
      products.sort((a, b) => {
        const x = a[key], y = b[key];
        return dir * (typeof x === 'string' ? x.localeCompare(y) : x - y);
      });
      render();
    });
  }

  render();
</script>
</body>
</html>
A sortable table built from a JSON block in the same page. Click a heading to sort.

If the JSON does live at its own address, the loading step is one line. r.json() reads the response body and parses it for you:

fetch('/data/products.json')
  .then((r) => r.json())
  .then((products) => render(products));

The file then needs to be served from somewhere another page can read it. Host a JSON file covers that.

Put parsed data on the page safely

Parsed data is text someone else wrote. If you build HTML by joining strings and assigning them to innerHTML, a value such as <img src=x onerror=...> becomes real markup and can run script.

Set text with textContent instead. The table above has a product called USB-C cable <2 m>, and it shows up literally, because textContent never reads its value as HTML. innerHTML explains when each one is the right choice.

When it does not work

What you see Cause Fix
Unexpected token < The text is HTML, often an error page or a 404, not JSON Log the raw text and check where it came from
Unexpected token ' or / Single quotes or a comment Double quotes only; remove comments
Expected double-quoted property name Trailing comma, or a key without quotes Remove the last comma; quote every key
"[object Object]" is not valid JSON The value is already an object Skip JSON.parse and use it directly
"undefined" is not valid JSON The variable or storage key is empty Check the value exists before parsing
Unexpected end of JSON input Empty string or text cut off Check for an empty value; look at the end of the text
Unexpected token with nothing visible in the quotes, in a file that looks fine A byte order mark (an invisible character, U+FEFF) at the start Save as UTF-8 without BOM, or strip \uFEFF first
NaN or Infinity became null JSON has no such numbers Store them as strings, or accept null
A key is missing after stringify Its value was undefined or a function Use null for an empty value
A date is a string after parsing JSON has no date type Convert it with a reviver

Spaces, tabs and line breaks around the JSON are allowed, so trailing newlines are not the problem. The byte order mark is different: it is not JSON whitespace, and JSON.parse rejects it.

A JSON tool is easier to hand over than to explain. Send someone the validator or the sortable table as a working page, and they can paste their own data into it instead of reading a screenshot.

Paste the page into a NOS document and choose Create share link. HTML to link walks through it. The page renders as written and its scripts run, so the people you send it to can use it directly.

If you change the data block or the code later, the same link shows the new version.

Questions people ask

What is the difference between JSON.parse and JSON.stringify?

JSON.parse reads JSON text and returns a JavaScript value, usually an object or an array. JSON.stringify does the opposite: it takes a value and returns a string of JSON text. Parse what you receive, stringify what you send or store.

Why does JSON.parse say "Unexpected token"?

The text breaks a JSON rule at that character. Common causes are single quotes, a comment, NaN, or text that is not JSON at all, such as an HTML error page that starts with <. Run the text through a validator to see where it fails.

Does JSON allow comments or trailing commas?

No. The JSON format has no comment syntax, and a comma after the last item of an object or array is an error. Some config file formats accept both, but JSON.parse does not.

How do I read JSON in an HTML page without fetch?

Put the JSON inside <script type="application/json" id="data"> in the page. The browser does not run that block. Read it with document.getElementById('data').textContent and pass the text to JSON.parse.

Why do my dates come back as strings?

JSON has no date type. JSON.stringify writes a Date as an ISO string, and JSON.parse returns that string as a string. Pass a reviver function to JSON.parse that turns matching strings back into Date objects.

Keep reading