Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

For a regular HTML table, the practical conversion is an array of objects: read the header cells as keys, pair each data cell with its key, then call JSON.stringify(). That shortcut is appropriate only when the table has one clear header row and one cell per column. Tables with rowspan, colspan, multi-level headings, repeated labels, or values that look numeric need an explicit policy before you serialize them.

The basic HTML table-to-JSON mapping

An HTML <table> can contain a caption, column groups, header, body and footer sections, and multiple rows. The browser exposes it through HTMLTableElement. JSON, by contrast, has no native table type, so you must choose a representation. The most useful application format is an array of row objects:

[{"Name":"Ada","Score":"98"},{"Name":"Linus","Score":"94"}]

That representation is a design choice, not an automatic meaning of HTML. Decide what to do with blank or duplicate headings, footer rows, nested markup, and cell values before writing the converter.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Convert a regular table in JavaScript

This browser-side function expects one header row made of th (or header-like cells) and regular body rows. It preserves values as strings, trims surrounding whitespace, gives blank headings generated names, and makes duplicate headings unique.

function tableToJson(table) {
  const headerCells = [...table.querySelectorAll('thead tr:first-child th, tr:first-child th')];
  if (!headerCells.length) throw new Error('No header cells found');

  const keys = [];
  const used = new Map();
  for (const cell of headerCells) {
    const base = cell.textContent.trim() || `column_${keys.length + 1}`;
    const count = (used.get(base) || 0) + 1;
    used.set(base, count);
    keys.push(count === 1 ? base : `${base}_${count}`);
  }

  const bodyRows = [...table.querySelectorAll('tbody tr')];
  const rows = bodyRows.length ? bodyRows : [...table.querySelectorAll('tr')].slice(1);
  return rows.map(row => {
    const cells = [...row.cells];
    const result = {};
    keys.forEach((key, index) => {
      result[key] = cells[index]?.textContent.trim() ?? '';
    });
    return result;
  });
}

const table = document.querySelector('#orders');
const data = tableToJson(table);
console.log(JSON.stringify(data, null, 2));

Use a specific selector such as #orders rather than assuming the first table on the page. If a page contains navigation, layout, pricing, and data tables, selecting the wrong element can still produce syntactically valid but useless JSON.

Choosing headings

  • Use visible heading text when it is the intended field label.
  • Normalize whitespace and decide whether punctuation, case, or accents should remain.
  • For an empty heading, supply a deterministic name such as column_3.
  • For repeated headings, suffix them (Status_2) or map them to an array. Never silently overwrite one value with another.

Choosing rows

Usually convert only tbody tr elements. If there is no tbody, explicitly skip the header row. Exclude totals, notes, and pagination rows unless they belong to your data contract. A footer total is not normally another record.

Handle values and types deliberately

textContent always starts as text. The string 0017 may be an identifier, not the number 17; 1,200 is locale-dependent; and an empty cell might mean an unknown value, zero, or an intentionally blank field. Do not coerce everything with Number().

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function parseValue(raw, type) {
  const value = raw.trim();
  if (value === '') return null; // change this policy if blank means empty string
  if (type === 'number') {
    const n = Number(value.replaceAll(',', ''));
    if (!Number.isFinite(n)) throw new Error(`Invalid number: ${value}`);
    return n;
  }
  if (type === 'boolean') return /^(true|yes|1)$/i.test(value);
  return value;
}

// Example schema: keys are normalized field names, values describe parsing.
const schema = { Name: 'string', Score: 'number', Active: 'boolean' };

Dates need a documented format and timezone. Currency needs a locale and decimal policy. If a conversion error matters, report the row, column, original text, and reason instead of quietly returning a misleading value.

Why spans and complex headers break the shortcut

In a visual table, rowspan and colspan make a rectangular grid appear even though some rows contain fewer physical cells. The cells[index] approach then assigns a value to the wrong key. Multi-row headers create another problem: “Revenue” under “2025” and “Revenue” under “2026” need distinct paths, not one duplicate property.

Expand a visual grid first

A robust converter places each cell into the next available slot in a matrix, honoring its row and column spans. It then derives headers from the completed header rows. The algorithm must also decide how overlapping spans are reported; malformed markup should be an error, not silently repaired.

function expandRows(rows) {
  const grid = [];
  rows.forEach((row, r) => {
    if (!grid[r]) grid[r] = [];
    let c = 0;
    [...row.cells].forEach(cell => {
      while (grid[r][c] !== undefined) c++;
      const rowSpan = Number(cell.getAttribute('rowspan') || 1);
      const colSpan = Number(cell.getAttribute('colspan') || 1);
      for (let rr = 0; rr < rowSpan; rr++) {
        if (!grid[r + rr]) grid[r + rr] = [];
        for (let cc = 0; cc < colSpan; cc++) {
          if (grid[r + rr][c + cc] !== undefined) {
            throw new Error(`Overlapping cell at row ${r + rr}, column ${c + cc}`);
          }
          grid[r + rr][c + cc] = cell;
        }
      }
      c += colSpan;
    });
  });
  return grid;
}

For accessible, multi-level headings, use header associations (scope, id, and headers) where present. A converter can build a key from the header path, for example 2025.Revenue, or accept a supplied schema. There is no universally correct key naming rule.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Standards-oriented JSON versus a DOM mapper

The W3C documents “Generating JSON from Tabular Data on the Web” and the related tabular-data model describe annotated tables, metadata, columns, rows, cells, parsing, and errors. They define minimal and standard conversion modes. The conversion document states: “A conformant JSON conversion application MUST produce output conforming to this algorithm according to the chosen mode of conversion: standard or minimal.” Those reports concern an annotated tabular-data model; they do not prescribe every ad hoc DOM-to-object mapping.

Use a simple mapper when you control the markup and need application records. Consider the standards model when metadata, annotations, typed parsing, provenance, or interoperability are requirements. State which mode or custom contract you implement, because two valid converters can produce different JSON shapes.

Decision Simple DOM mapping Standards-oriented conversion
Input Selected parsed DOM or HTML string Annotated tabular-data model
Output Usually an array of row objects Mode-defined JSON with table metadata
Headers One row, with your duplicate-name policy Header paths and annotations can be represented
Types Your parser, often strings by default Model parsing and cell errors
Best fit Internal scripts and controlled pages Portable, metadata-rich pipelines

Using a library

The npm package tabletojson documents conversion from HTML markup or a URL and options for duplicate headings, row and column spans, complex headers, HTML inside cells, ignored columns, and row limits. Those options are useful checkpoints when evaluating whether it fits your input; verify its current version and runtime behavior, then validate the output against your own schema. A library does not remove ambiguity in headings, types, hidden rows, or totals.

Remote pages and dynamically rendered tables

If the source is a URL, determine whether the table exists in the original HTML or is inserted by JavaScript. A server-side HTTP request may see only an empty container. In that case, run the page in a browser automation environment, wait for a selector or network idle, and then query the rendered DOM. Respect authentication, robots policies, rate limits, and the site’s terms. Never treat a browser extension’s ability to see a table as proof that an unattended server can access it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Other practical workflows

  • Saved HTML: parse it with a DOM implementation, select the intended table, and apply the same schema and type rules.
  • Several tables: identify each by an id, caption, heading text, or surrounding landmark; return a named object rather than guessing.
  • Visible-grid export: a browser extension may export tables, including some rendered grids. Its feature and privacy statements are vendor claims, so evaluate the page and data before using it.
  • HTML in cells: choose whether to preserve markup, extract text, or sanitize selected links. Do not insert extracted HTML into a page without sanitization.

Or skip the browser setup

If your real task is obtaining a clean image or PDF of the page rather than extracting cell records, ScreenshotNeo makes one HTTP request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for options such as full-page capture, CSS selectors, custom JavaScript, waits, headers, cookies, device presets, PDF ranges, async jobs, and bulk capture. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Python and Node.js examples

These examples call the same endpoint. They are useful when your pipeline already has the URL and needs an image or PDF artifact alongside extracted data.

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Troubleshooting checklist

“No header cells found”

The table may use td for headings, place headers in a different row, or be a layout table. Inspect the DOM and select the actual header row; do not infer headings from visual position alone.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Columns shift after a span

Use a span-aware grid expansion algorithm or a library that explicitly supports row and column spans. Compare the expanded matrix with the browser’s visual table.

Duplicate keys disappear

Assign unique keys or nested header paths before constructing objects. JavaScript object assignment overwrites an earlier property with the same name.

Numbers or dates are wrong

Preserve strings first, then parse with a locale- and schema-specific function. Log the original value and row when parsing fails.

The result is empty

The table may be rendered after the initial response, behind a login, inside an iframe, or loaded only after scrolling. Use a browser context, wait for a meaningful selector, and verify access before conversion.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Unexpected extra records

Totals, footers, hidden rows, or pagination controls may match your selector. Restrict extraction to the intended section and exclude rows by class, role, or an explicit predicate.

Validation before shipping

  • Test an empty table, a missing cell, a blank heading, and duplicate headings.
  • Test rowspan, colspan, nested elements, HTML entities, and multiple header rows.
  • Assert the expected key set and column count for every record.
  • Keep a sample of source HTML with the converted JSON for regression tests.
  • Define whether whitespace, blanks, numbers, dates, booleans, errors, totals, and hidden rows are preserved or transformed.

Frequently Asked Questions

Does JSON.stringify convert an HTML table automatically?

No. It serializes the JavaScript value you provide; you must first read the table and construct an array, object, or other representation.

Should I scrape table text with innerHTML or textContent?

Use textContent when you need cell text. Preserve innerHTML only when markup is intentionally part of your data and you have a sanitization policy.

Can a browser extension convert any table?

No. Extensions depend on the page’s DOM and their own handling of rendered grids, spans, headers, and permissions. Validate the exported structure and the publisher’s current privacy claims.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.