How JSON becomes rows and columns
Each item of the top-level array becomes one row. A single object becomes one row, and a value that is not an object (a number or a string in the array) goes into a column named value. Every item is flattened first:
- Nested objects become one column per property, with the names joined by dots:
{"address": {"city": "Belgrade"}}gives the columnaddress.city. - Strings are written without the JSON quotes and escapes, numbers exactly as they are in the JSON (
1.50stays1.50),trueandfalseas text, andnullas an empty cell. - Columns are listed in the order they first appear. An item without a property gets an empty cell, so objects with different shapes still line up.
- An empty object adds no column, and neither does an empty array with one column per item. Joined, it is an empty cell; as JSON text,
[].
Arrays: indexed, joined or JSON
CSV has no lists, so an array has to become something flat. For "roles": ["admin", "editor"]:
| Arrays option | Columns | Cells |
|---|---|---|
| Per item | roles.0, roles.1 | admin, editor |
| Joined | roles | admin; editor |
| JSON text | roles | ["admin","editor"] |
With one column per item, arrays of objects flatten all the way down: items.0.sku, items.0.quantity, items.1.sku. When joining, an object inside the array is written as compact JSON. JSON text is written with UnsafeRelaxedJsonEscaping, so é stays é.
Quoting and line endings, as CsvHelper writes them
CsvWriter puts a field in double quotes only when it has to, and doubles any quote inside it (RFC 4180). A field is quoted when it:
- contains the delimiter, a double quote, or a line break (
\ror\n); - starts or ends with a space.
Empty fields are never quoted. "Every field" sets ShouldQuote = _ => true, which also writes empty fields as "". Records end with \r\n, as RFC 4180 says, on every OS. When you set NewLine = "\n", CsvHelper only quotes fields that contain \n: a lone \r is then written without quotes.
For the other direction, use the CSV to JSON converter. To check a CSV file before you import it, open it in the CSV viewer.
Convert JSON to CSV in C#
The code follows the settings above (it needs the CsvHelper NuGet package) and gives the same output as this page:
using System.Globalization;
using System.Text.Json;
using CsvHelper;
using CsvHelper.Configuration;
static string JsonToCsv(string json)
{
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
var items = root.ValueKind == JsonValueKind.Array ? root.EnumerateArray().ToList() : [root];
// One dictionary per item: nested properties become "parent.child" columns.
var rows = items.Select(item =>
{
var row = new Dictionary<string, string>();
Flatten(item, "", row);
return row;
}).ToList();
var headers = rows.SelectMany(row => row.Keys).Distinct().ToList();
var config = new CsvConfiguration(CultureInfo.InvariantCulture);
using var writer = new StringWriter();
using (var csv = new CsvWriter(writer, config))
{
foreach (var header in headers) csv.WriteField(header);
csv.NextRecord();
foreach (var row in rows)
{
foreach (var header in headers) csv.WriteField(row.GetValueOrDefault(header, ""));
csv.NextRecord();
}
}
return writer.ToString();
}
static void Flatten(JsonElement element, string path, Dictionary<string, string> row)
{
string Key(string name) => path.Length == 0 ? name : $"{path}.{name}";
switch (element.ValueKind)
{
case JsonValueKind.Object:
foreach (var property in element.EnumerateObject()) Flatten(property.Value, Key(property.Name), row);
break;
case JsonValueKind.Array: // one column per item: tags.0, tags.1, ...
for (var i = 0; i < element.GetArrayLength(); i++) Flatten(element[i], Key(i.ToString()), row);
break;
default:
row[path.Length == 0 ? "value" : path] = CellText(element);
break;
}
}
static string CellText(JsonElement element) => element.ValueKind switch
{
JsonValueKind.String => element.GetString()!,
JsonValueKind.Null => "",
_ => element.GetRawText(), // numbers as written, true, false
};
Tested with CsvHelper 33.1 on .NET 10. To write a file instead of a string, pass a StreamWriter to CsvWriter. For your own classes you do not need the flattening: csv.WriteRecords(orders) writes one column per property, and a ClassMap renames or reorders them.
FAQ
How do I convert JSON to CSV in C#?
Parse the JSON with JsonDocument.Parse, flatten each object into a dictionary of column name to text, collect the column names in order, and write the header and the rows with CsvHelper's CsvWriter. The C# on this page does exactly that and gives the same output as the converter.
How are nested objects and arrays converted?
A nested object becomes one column per property, named with dots: address.city. Arrays can become one column per item (tags.0, tags.1), one column with the items joined by a separator, or one column with the array as JSON text.
When does CsvHelper put quotes around a field?
When the field contains the delimiter, a double quote or a line break, or starts or ends with a space. Quotes inside the field are doubled. Set ShouldQuote = _ => true to quote every field.
Why does Excel show é as garbled characters?
Excel reads a CSV file without a byte order mark in the system code page. Save it as UTF-8 with a BOM, for example File.WriteAllText(path, csv, Encoding.UTF8), and Excel reads the accents correctly. In many European locales Excel also expects a semicolon as the delimiter.
Does CsvHelper protect against CSV injection?
Not by default: a cell that starts with =, +, - or @ is written as it is, and a spreadsheet may run it as a formula. Set InjectionOptions = InjectionOptions.Escape on the configuration when the data comes from users.
Is my JSON uploaded?
No. The conversion happens in your browser, so your JSON stays on your machine.