Data

CSV JSON

Convert CSV to JSON or JSON to CSV. Supports nested/multilevel JSON flattening, custom delimiters, quoted fields, and drag & drop.

📂
Drop a CSV file here
or click to browse · .csv, .tsv, .txt
JSON Output
Rows: 0 Columns: 0 Size:
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

/* ── Tab switching ─────────────────────────────────────── */
  function switchTab(tab) {
    document.getElementById('panel-csv2json').classList.toggle('hidden', tab !== 'csv2json');
    document.getElementById('panel-json2csv').classList.toggle('hidden', tab !== 'json2csv');
    document.getElementById('tab-csv2json').classList.toggle('active', tab === 'csv2json');
    document.getElementById('tab-json2csv').classList.toggle('active', tab === 'json2csv');
  }

  /* ── File drag & drop ──────────────────────────────────── */
  function onDragOver(e, id) { e.preventDefault(); document.getElementById(id).classList.add('dragover'); }
  function onDragLeave(id) { document.getElementById(id).classList.remove('dragover'); }
  function onDrop(e, mode) {
    e.preventDefault();
    const id = mode === 'csv2json' ? 'dropZoneA' : 'dropZoneB';
    document.getElementById(id).classList.remove('dragover');
    const file = e.dataTransfer.files[0];
    if (file) readFile(file, mode);
  }
  function loadFile(e, mode) {
    const file = e.target.files[0];
    if (file) readFile(file, mode);
  }
  function readFile(file, mode) {
    const nameEl = document.getElementById(mode === 'csv2json' ? 'fileNameA' : 'fileNameB');
    nameEl.textContent = file.name + ' (' + fmtBytes(file.size) + ')';
    nameEl.style.display = 'block';
    const reader = new FileReader();
    reader.onload = e => {
      if (mode === 'csv2json') {
        document.getElementById('csvInput').value = e.target.result;
        convertCSV();
      } else {
        document.getElementById('jsonInput').value = e.target.result;
        convertJSON();
      }
    };
    reader.readAsText(file);
  }

  /* ── CSV parser ────────────────────────────────────────── */
  function parseCSV(text, delim) {
    const rows = [];
    let row = [], field = '', inQuotes = false;
    for (let i = 0; i < text.length; i++) {
      const ch = text[i], next = text[i + 1];
      if (inQuotes) {
        if (ch === '"' && next === '"') { field += '"'; i++; }
        else if (ch === '"') { inQuotes = false; }
        else { field += ch; }
      } else {
        if (ch === '"') { inQuotes = true; }
        else if (ch === delim) { row.push(field); field = ''; }
        else if (ch === '\n' || (ch === '\r' && next === '\n')) {
          row.push(field); field = '';
          if (row.some(c => c !== '')) rows.push(row);
          row = [];
          if (ch === '\r') i++;
        } else if (ch === '\r') {
          row.push(field); field = '';
          if (row.some(c => c !== '')) rows.push(row);
          row = [];
        } else { field += ch; }
      }
    }
    if (field !== '' || row.length) { row.push(field); if (row.some(c => c !== '')) rows.push(row); }
    return rows;
  }

  /* ── CSV → JSON ────────────────────────────────────────── */
  function convertCSV() {
    const text = document.getElementById('csvInput').value.trim();
    const delim = document.getElementById('csvDelim').value;
    const useKeys = document.getElementById('firstRowKeys').checked;
    const trim = document.getElementById('trimWhitespace').checked;
    const minify = document.getElementById('minifyJson').checked;
    const errEl = document.getElementById('csvError');
    errEl.classList.add('hidden');

    if (!text) { resetOutput('csv2json'); return; }

    try {
      const rows = parseCSV(text, delim);
      if (!rows.length) throw new Error('No data found.');
      let result, cols;
      if (useKeys) {
        const keys = rows[0].map(k => trim ? k.trim() : k);
        cols = keys.length;
        result = rows.slice(1).map(row => {
          const obj = {};
          keys.forEach((k, i) => { obj[k] = row[i] !== undefined ? (trim ? row[i].trim() : row[i]) : ''; });
          return obj;
        });
        document.getElementById('statRowsA').textContent = result.length;
      } else {
        cols = rows[0].length;
        result = trim ? rows.map(r => r.map(f => f.trim())) : rows;
        document.getElementById('statRowsA').textContent = rows.length;
      }
      document.getElementById('statColsA').textContent = cols;
      const json = minify ? JSON.stringify(result) : JSON.stringify(result, null, 2);
      document.getElementById('jsonOutput').textContent = json;
      document.getElementById('statSizeA').textContent = fmtBytes(new Blob([json]).size);
    } catch (e) {
      errEl.textContent = 'Error: ' + e.message;
      errEl.classList.remove('hidden');
      resetOutput('csv2json');
    }
  }

  /* ── Flatten nested objects ──────────────────────────────── */
  function flattenObject(obj, sep, prefix) {
    const result = {};
    for (const key of Object.keys(obj)) {
      const fullKey = prefix ? prefix + sep + key : key;
      const val = obj[key];
      if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
        // Nested object — recurse
        Object.assign(result, flattenObject(val, sep, fullKey));
      } else if (Array.isArray(val)) {
        if (val.length === 0) {
          result[fullKey] = '';
        } else if (val.every(v => v === null || typeof v !== 'object')) {
          // All-primitive array — join as comma-separated string
          result[fullKey] = val.map(v => v === null ? '' : String(v)).join(', ');
        } else {
          // Mixed or object array — flatten each element by index
          val.forEach((item, i) => {
            const indexKey = fullKey + sep + i;
            if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
              Object.assign(result, flattenObject(item, sep, indexKey));
            } else if (Array.isArray(item)) {
              // Nested array — JSON-stringify it
              result[indexKey] = JSON.stringify(item);
            } else {
              result[indexKey] = item === null ? '' : item;
            }
          });
        }
      } else {
        result[fullKey] = val;
      }
    }
    return result;
  }

  /* ── JSON → CSV ────────────────────────────────────────── */
  function convertJSON() {
    const text = document.getElementById('jsonInput').value.trim();
    const delim = document.getElementById('jsonDelim').value;
    const header = document.getElementById('includeHeader').checked;
    const quoteAll = document.getElementById('quoteAll').checked;
    const flatten = document.getElementById('flattenNested').checked;
    const nestSep = document.getElementById('nestSep').value;
    const errEl = document.getElementById('jsonError');
    errEl.classList.add('hidden');
    errEl.style.cssText = '';

    if (!text) { resetOutput('json2csv'); return; }

    try {
      let data = JSON.parse(text);

      // Auto-unwrap common API wrapper shapes: {data:[...]}, {results:[...]}, etc.
      const WRAPPER_KEYS = ['data','results','items','records','rows','users','list','entries','payload','response','content'];
      if (!Array.isArray(data) && typeof data === 'object' && data !== null) {
        const keys = Object.keys(data);
        const wrapKey = keys.find(k => WRAPPER_KEYS.includes(k.toLowerCase()) && Array.isArray(data[k]));
        if (wrapKey) {
          data = data[wrapKey];
        } else {
          // Single object — wrap it
          data = [data];
        }
      }

      if (!Array.isArray(data)) throw new Error('JSON must be an array of objects or a recognized wrapper object.');
      if (!data.length) throw new Error('JSON array is empty.');

      // Filter out null/non-object items gracefully
      const nullCount = data.filter(r => r === null || typeof r !== 'object' || Array.isArray(r)).length;
      data = data.filter(r => r !== null && typeof r === 'object' && !Array.isArray(r));
      if (!data.length) throw new Error('No valid object items found in array.');

      // Flatten nested objects if enabled
      if (flatten) {
        data = data.map(row => flattenObject(row, nestSep, ''));
      }

      // Collect all keys (union across all rows, preserving insertion order)
      const keysSet = new Set();
      data.forEach(row => { Object.keys(row).forEach(k => keysSet.add(k)); });
      const keys = [...keysSet];
      if (!keys.length) throw new Error('Objects have no keys — nothing to convert.');

      // Warn about skipped rows
      if (nullCount > 0) {
        errEl.textContent = `⚠️ ${nullCount} non-object item(s) in the array were skipped.`;
        errEl.style.cssText = 'background:rgba(255,180,0,0.08);border-color:rgba(255,180,0,0.35);color:#e6b800;';
        errEl.classList.remove('hidden');
      }

      function escapeField(val) {
        let s;
        if (val === null || val === undefined) {
          s = '';
        } else if (typeof val === 'object') {
          // Arrays and objects: serialize to JSON string rather than [object Object]
          s = JSON.stringify(val);
        } else {
          s = String(val);
        }
        const needsQuote = quoteAll || s.includes(delim) || s.includes('"') || s.includes('\n') || s.includes('\r');
        if (needsQuote) return '"' + s.replace(/"/g, '""') + '"';
        return s;
      }

      const lines = [];
      if (header) lines.push(keys.map(k => escapeField(k)).join(delim));
      data.forEach(row => {
        lines.push(keys.map(k => escapeField(row[k])).join(delim));
      });
      const csv = lines.join('\n');
      document.getElementById('csvOutput').textContent = csv;
      document.getElementById('statRowsB').textContent = data.length;
      document.getElementById('statColsB').textContent = keys.length;
      document.getElementById('statSizeB').textContent = fmtBytes(new Blob([csv]).size);
    } catch (e) {
      errEl.textContent = 'Error: ' + e.message;
      errEl.classList.remove('hidden');
      resetOutput('json2csv');
    }
  }

  /* ── Helpers ───────────────────────────────────────────── */
  function resetOutput(mode) {
    if (mode === 'csv2json') {
      document.getElementById('jsonOutput').textContent = '—';
      document.getElementById('statRowsA').textContent = '0';
      document.getElementById('statColsA').textContent = '0';
      document.getElementById('statSizeA').textContent = '—';
    } else {
      document.getElementById('csvOutput').textContent = '—';
      document.getElementById('statRowsB').textContent = '0';
      document.getElementById('statColsB').textContent = '0';
      document.getElementById('statSizeB').textContent = '—';
    }
  }

  function fmtBytes(b) {
    if (b < 1024) return b + ' B';
    if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KB';
    return (b / (1024 * 1024)).toFixed(2) + ' MB';
  }

  function copyOutput(outputId, btnId) {
    const text = document.getElementById(outputId).textContent;
    if (text === '—') return;
    navigator.clipboard.writeText(text).catch(() => {});
    const btn = document.getElementById(btnId);
    btn.textContent = 'copied!'; btn.classList.add('copied');
    setTimeout(() => { btn.textContent = 'copy'; btn.classList.remove('copied'); }, 2000);
  }

  function downloadOutput(outputId, filename, mime) {
    const text = document.getElementById(outputId).textContent;
    if (text === '—') return;
    const blob = new Blob([text], { type: mime });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    a.click();
    setTimeout(() => URL.revokeObjectURL(a.href), 5000);
  }

  function clearPanel(mode) {
    if (mode === 'csv2json') {
      document.getElementById('csvInput').value = '';
      document.getElementById('csvError').classList.add('hidden');
      document.getElementById('fileNameA').style.display = 'none';
      document.getElementById('fileInputA').value = '';
    } else {
      document.getElementById('jsonInput').value = '';
      document.getElementById('jsonError').classList.add('hidden');
      document.getElementById('fileNameB').style.display = 'none';
      document.getElementById('fileInputB').value = '';
    }
    resetOutput(mode);
  }