Skip to content

JSON ↔ CSV Converter — TypeScript 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 TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// JSON <-> CSV conversion (RFC 4180 quoting). Pure + deterministic - the
// unit-test surface for the JSON <-> CSV Converter tool. csvToRows is the raw
// parser (shared with csv-to-sql, which needs rows before headers: duplicate
// and empty header names are lossy through a Record).

function csvEscape(field: unknown): string {
  const s = field == null ? '' : String(field);
  if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
  return s;
}

export function jsonToCsv(json: string): string | null {
  let data: unknown;
  try { data = JSON.parse(json); } catch { return null; }
  const rows = Array.isArray(data) ? data : [data];
  const headers: string[] = [];
  for (const row of rows) {
    if (row && typeof row === 'object') {
      for (const k of Object.keys(row as object)) if (!headers.includes(k)) headers.push(k);
    }
  }
  if (headers.length === 0) return null;
  const lines = [headers.map(csvEscape).join(',')];
  for (const row of rows) {
    const obj = row && typeof row === 'object' ? (row as Record<string, unknown>) : {};
    lines.push(headers.map((h) => csvEscape(obj[h])).join(','));
  }
  return lines.join('\n');
}

export function csvToRows(csv: string): string[][] {
  const rows: string[][] = [];
  let field = '';
  let row: string[] = [];
  let inQ = false;
  for (let i = 0; i < csv.length; i++) {
    const ch = csv[i];
    if (inQ) {
      if (ch === '"') {
        if (csv[i + 1] === '"') { field += '"'; i++; } else inQ = false;
      } else field += ch;
    } else if (ch === '"') {
      inQ = 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;
    }
  }
  if (field.length > 0 || row.length > 0) { row.push(field); rows.push(row); }
  return rows;
}

export function csvToJson(csv: string): Record<string, string>[] {
  const rows = csvToRows(csv);
  if (rows.length === 0) return [];
  const headers = rows[0];
  return rows.slice(1).map((r) => {
    const obj: Record<string, string> = {};
    headers.forEach((h, i) => { obj[h] = r[i] ?? ''; });
    return obj;
  });
}

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 →