JSON ↔ CSV Converter — C# 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 C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// =============================================================================
// json-csv — C# 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 a list of row records
//
// Language: C# 12 — System.Text.Json from the .NET runtime.
// Source: CosmoDev polyglot showcase port of json-csv,
// ported from src/lib/csv.ts (the canonical, live TypeScript lib).
// License: display source — part of CosmoDev's polyglot tool pages.
//
// 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.
// =============================================================================
// JSON parsing uses System.Text.Json (in the .NET runtime — the C# equivalent
// of an ecosystem crate in Rust). JsonDocument preserves property order, which
// matters here: key order is observable (it determines CSV column order).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
namespace CosmoDev.JsonCsv;
public static class JsonCsv
{
// ---------------------------------------------------------------------------
// Number formatting (JS String(number) parity)
// ---------------------------------------------------------------------------
// C#'s `30.0.ToString()` is "30" (like JavaScript's `String(30.0)`), and
// "R" gives the shortest round-trip form for non-integral doubles. The
// explicit integral branch documents the parity and dodges culture drift.
static string FormatNumber(double n)
{
if (n == Math.Floor(n) && Math.Abs(n) < 1e16)
{
return ((long)n).ToString(System.Globalization.CultureInfo.InvariantCulture);
}
return n.ToString("R", System.Globalization.CultureInfo.InvariantCulture);
}
// ---------------------------------------------------------------------------
// JS-equivalent value semantics
// ---------------------------------------------------------------------------
// The canonical lib uses `typeof x === 'object'` and Object.keys(x), which in
// JavaScript treat BOTH objects and arrays as "object" and expose array indices
// as string keys ("0", "1", ...). We mirror that so degenerate inputs (e.g. an
// array of arrays) produce byte-identical output to the TS.
/// Object.keys parity: array indices as strings ("0", "1", ...), or the
/// object's insertion-ordered keys. Primitives and null yield no keys.
static IEnumerable<string> KeysOf(JsonElement v)
{
switch (v.ValueKind)
{
case JsonValueKind.Array:
for (var i = 0; i < v.GetArrayLength(); i++)
{
yield return i.ToString();
}
break;
case JsonValueKind.Object:
foreach (var p in v.EnumerateObject())
{
yield return p.Name;
}
break;
}
}
/// JS `obj[key]` parity: object lookup, or array element at a non-negative
/// integer index. Returns null when absent (which renders as the empty field).
static JsonElement? GetField(JsonElement v, string key)
{
switch (v.ValueKind)
{
case JsonValueKind.Object:
foreach (var p in v.EnumerateObject())
{
if (p.Name == key)
{
return p.Value;
}
}
return null;
case JsonValueKind.Array:
if (int.TryParse(key, out var i) && i >= 0 && i < v.GetArrayLength())
{
return v[i];
}
return null;
default:
return null;
}
}
/// Coerce a JSON value to its display string, replicating JavaScript's
/// String(): null -> "", booleans -> "true"/"false", numbers -> decimal
/// form, arrays -> elements joined by "," (so a comma-bearing cell
/// re-quotes), and objects -> "[object Object]".
static string JsString(JsonElement v) => v.ValueKind switch
{
JsonValueKind.Null or JsonValueKind.Undefined => "",
JsonValueKind.True => "true",
JsonValueKind.False => "false",
JsonValueKind.Number => FormatNumber(v.GetDouble()),
JsonValueKind.String => v.GetString() ?? "",
JsonValueKind.Array => string.Join(",", v.EnumerateArray().Select(JsString)),
_ => "[object Object]", // Object (or anything else)
};
/// Quote a single CSV field per RFC 4180.
static string CsvEscape(JsonElement v)
{
var s = JsString(v);
if (s.IndexOfAny([',', '"', '\n', '\r']) >= 0)
{
return $"\"{s.Replace("\"", "\"\"")}\"";
}
return s;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// One deserialized CSV record: an ordered list of (header, cell) pairs. We
/// use a list rather than a dictionary so duplicate/empty headers survive
/// round-trips, exactly as in the TS lib's `Record<string, string>` indexing.
public sealed record CsvRow(IReadOnlyList<(string Header, string Cell)> Fields)
{
public string this[string header]
{
get
{
var f = Fields.FirstOrDefault(x => x.Header == header);
return f == default ? "" : f.Cell;
}
}
}
/// Serialize a JSON document to CSV.
///
/// Accepts a single object or an array of objects. Returns null on invalid
/// JSON, or when the document yields no object rows (and thus no column
/// headers) — e.g. a bare array of primitives such as <c>[1, 2, 3]</c>.
public static string? JsonToCsv(string text)
{
JsonDocument doc;
try
{
doc = JsonDocument.Parse(text);
}
catch (JsonException)
{
return null;
}
using (doc)
{
var data = doc.RootElement;
// A bare value is treated as a one-row table.
var rows = data.ValueKind == JsonValueKind.Array
? data.EnumerateArray().ToList()
: [data];
// Header union across object-like rows, first-seen order, de-duplicated.
var headers = new List<string>();
var seen = new HashSet<string>();
foreach (var row in rows)
{
foreach (var k in KeysOf(row))
{
if (seen.Add(k))
{
headers.Add(k);
}
}
}
if (headers.Count == 0)
{
return null;
}
// First line is the (escaped) header row; subsequent lines are the rows.
var lines = new List<string>(rows.Count + 1)
{
string.Join(",", headers.Select(h => CsvEscape((JsonElement)h))),
};
foreach (var row in rows)
{
// A non-object row (null, number, string) yields an empty line:
// every header lookup on it returns null -> the empty field.
lines.Add(string.Join(",", headers.Select(h =>
CsvEscape(GetField(row, h) ?? default)));
}
return string.Join("\n", lines);
}
}
/// Parse RFC 4180 CSV into a list of rows keyed by the first row.
///
/// Handles quoted fields, doubled-quote escapes, and embedded
/// commas/newlines; bare carriage returns outside quotes are ignored.
/// Returns an empty list for empty input, or for input that is only a
/// header row.
public static List<CsvRow> CsvToJson(string text)
{
// Single-pass character-state machine. `text` is indexed by position so
// we can look one character ahead for the doubled-quote escape.
var rows = new List<List<string>>();
var field = new StringBuilder();
var row = new List<string>();
var inQuotes = false;
var n = text.Length;
for (var i = 0; i < n; i++)
{
var ch = text[i];
if (inQuotes)
{
if (ch == '"')
{
// Doubled quote -> one literal quote; lone quote -> close field.
if (i + 1 < n && text[i + 1] == '"')
{
field.Append('"');
i++;
continue;
}
inQuotes = false;
}
else
{
field.Append(ch);
}
}
else if (ch == '"')
{
inQuotes = true;
}
else if (ch == ',')
{
row.Add(field.ToString());
field.Clear();
}
else if (ch == '\n')
{
row.Add(field.ToString());
rows.Add(row);
row = [];
field.Clear();
}
else if (ch != '\r')
{
field.Append(ch);
}
}
// Flush a trailing row only when there is pending content. Input that
// ended with a newline already flushed; this guard avoids an empty
// final row.
if (field.Length > 0 || row.Count > 0)
{
row.Add(field.ToString());
rows.Add(row);
}
if (rows.Count == 0)
{
return [];
}
var headers = rows[0];
var result = new List<CsvRow>(rows.Count - 1);
foreach (var r in rows.Skip(1))
{
CsvRow rec = new(headers.Select((h, i) => (h, i < r.Count ? r[i] : "")).ToList());
result.Add(rec);
}
return result;
}
}
public static class JsonCsvDemo
{
public static void Main()
{
// Small end-to-end demo so this file is runnable as a showcase.
const string raw = "[{\"name\":\"Doe, John\",\"note\":\"say \\\"hi\\\"\"},{\"name\":\"Jane\",\"note\":\"plain\"}]";
var csv = JsonCsv.JsonToCsv(raw);
Console.WriteLine(csv);
foreach (var row in JsonCsv.CsvToJson(csv ?? ""))
{
Console.WriteLine(string.Join(", ", row.Fields.Select(f => $"{f.Header}={f.Cell}")));
}
}
}
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 →