Skip to content

JSON ↔ CSV Converter — JavaScript source

Convert a JSON array of objects to CSV and back. Handles quoted fields, embedded commas, newlines and escaped quotes (RFC 4180). 100% in-browser.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// =============================================================================
// json-csv - JavaScript port
// =============================================================================
// Convert between JSON and RFC 4180 CSV in either direction:
//   • jsonToCsv - serialize a JSON document (object or array of objects) to CSV
//   • csvToJson - parse RFC 4180 CSV (with quoting) into an array of objects
//
// CosmoDev polyglot showcase port of the `json-csv` tool.
// Ported from src/lib/csv.ts (the canonical, live TypeScript lib).
//
// Pure and deterministic - depends only on its inputs. RFC 4180 quoting: any
// field containing a comma, double quote, carriage return, or line feed is
// wrapped in double quotes, and each embedded quote is doubled ("").
//
// This is display source - part of CosmoDev's polyglot tool pages.
// =============================================================================

// A field must be quoted when it contains any of: comma, double quote, CR, LF.
const NEEDS_QUOTING = /[",\n\r]/;

// Quote a single CSV field per RFC 4180. null/undefined become the empty field;
// every other value is stringified the way JavaScript's `String()` would, so a
// number, boolean, or nested structure renders exactly as it does in the TS lib
// (e.g. an array cell joins its elements with "," and is therefore re-quoted).
function csvEscape(field) {
  const s = field == null ? '' : String(field);
  if (NEEDS_QUOTING.test(s)) {
    return '"' + s.replace(/"/g, '""') + '"';
  }
  return s;
}

// Serialize a JSON document to CSV.
//
// Accepts either a single object or an array of objects. Returns null when the
// input is not valid JSON, or when the document yields no object rows (and thus
// no column headers) - e.g. a bare array of primitives such as `[1, 2, 3]`.
function jsonToCsv(json) {
  let data;
  try {
    data = JSON.parse(json);
  } catch {
    return null;
  }

  // A bare value is treated as a one-row table.
  const rows = Array.isArray(data) ? data : [data];

  // The header set is the union of keys across every object-like row, kept in
  // first-seen order. JavaScript's `typeof` counts both objects and arrays as
  // "object", so an array row exposes its indices ("0", "1", ...) as keys -
  // matching Object.keys behaviour. Null is excluded by the truthiness guard.
  const headers = [];
  for (const row of rows) {
    if (row && typeof row === 'object') {
      for (const key of Object.keys(row)) {
        if (!headers.includes(key)) headers.push(key);
      }
    }
  }
  if (headers.length === 0) return null;

  const lines = [headers.map(csvEscape).join(',')];
  for (const row of rows) {
    // A non-object row (null, number, string) contributes an empty line, since
    // every header lookup on it returns undefined -> the empty field.
    const obj = row && typeof row === 'object' ? row : {};
    lines.push(headers.map((h) => csvEscape(obj[h])).join(','));
  }
  return lines.join('\n');
}

// Parse RFC 4180 CSV into an array of row objects keyed by the first row.
//
// Implements a single-pass character-state machine: quoted fields may contain
// commas, newlines, and doubled-quote escapes; bare carriage returns outside
// quotes are ignored (so CRLF and LF both terminate rows cleanly). Returns []
// for empty input, or for input that is only a header row.
function csvToJson(csv) {
  const rows = [];
  let field = '';
  let row = [];
  let inQuotes = false;

  for (let i = 0; i < csv.length; i++) {
    const ch = csv[i];
    if (inQuotes) {
      if (ch === '"') {
        // A doubled quote inside a quoted field is one literal quote; a lone
        // quote closes the field.
        if (csv[i + 1] === '"') {
          field += '"';
          i++;
        } else {
          inQuotes = false;
        }
      } else {
        field += ch;
      }
    } else if (ch === '"') {
      inQuotes = true;
    } else if (ch === ',') {
      row.push(field);
      field = '';
    } else if (ch === '\n') {
      row.push(field);
      rows.push(row);
      row = [];
      field = '';
    } else if (ch !== '\r') {
      field += ch;
    }
  }

  // Flush a trailing row only when there is pending content or accumulated
  // cells. Input that ended with a newline has already flushed, so this guard
  // avoids manufacturing a spurious empty final row.
  if (field.length > 0 || row.length > 0) {
    row.push(field);
    rows.push(row);
  }

  if (rows.length === 0) return [];

  const headers = rows[0];
  return rows.slice(1).map((r) => {
    const obj = {};
    headers.forEach((h, i) => {
      obj[h] = r[i] ?? '';
    });
    return obj;
  });
}

// ---- Example usage (this file is a library; uncomment to run as a script) ---
// const csv = jsonToCsv('[{"name":"Doe, John","note":"say \\"hi\\""},{"name":"Jane","note":"plain"}]');
// console.log(csv);
// console.log(csvToJson(csv));

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →