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 →