All articles

Convert a DataTable to CSV, List or JSON in .NET

Three DataTable conversions done right in .NET 10: RFC-4180 CSV with quoting, typed List<T> via DataRow.Field, and JSON through records — edge cases verified.

0 · log in to like, save & follow Share on LinkedIn Share on X
Convert a DataTable to CSV, List or JSON in .NET

DataTable refuses to die — it arrives from legacy data layers, reporting stored procedures, Excel readers, and third-party SDKs. What modern code needs is CSV for exports, List<T> for LINQ and APIs, and JSON for the wire. This post implements all three conversions in plain .NET 10 — no extra packages — with the edge cases (embedded commas, quotes, typing) handled properly and outputs verified from a real run.

Convert a DataTable to CSV, List or JSON in .NET

The sample table

Deliberately hostile data — a name with quotes, a name with a comma:

var table = new DataTable("Products");
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
table.Columns.Add("Price", typeof(decimal));
table.Rows.Add(1, "Mechanical keyboard", 89.00m);
table.Rows.Add(2, "4K \"UltraSharp\" monitor", 329.99m);
table.Rows.Add(3, "USB-C dock, 11-in-1", 149.50m);

DataTable → CSV, correctly

Naïve string.Join(",") produces broken files the moment data contains a comma. RFC 4180 rules are simple: quote fields containing commas, quotes, or newlines; double the embedded quotes.

static string ToCsv(DataTable table)
{
    static string Escape(object? value)
    {
        var s = value?.ToString() ?? "";
        return s.Contains(',') || s.Contains('"') || s.Contains('\n')
            ? $"\"{s.Replace("\"", "\"\"")}\"" : s;
    }
    var sb = new StringBuilder();
    sb.AppendLine(string.Join(",", table.Columns.Cast<DataColumn>().Select(c => Escape(c.ColumnName))));
    foreach (DataRow row in table.Rows)
        sb.AppendLine(string.Join(",", row.ItemArray.Select(Escape)));
    return sb.ToString();
}

Verified output — note both hostile rows survive:

Id,Name,Price
1,Mechanical keyboard,89.00
2,"4K ""UltraSharp"" monitor",329.99
3,"USB-C dock, 11-in-1",149.50

Two production notes: write files with File.WriteAllText(path, csv, new UTF8Encoding(true)) if Excel is the consumer (the BOM makes Excel detect UTF-8), and for millions of rows stream with a StreamWriter per line instead of one giant StringBuilder. If your CSV needs get complex (culture-specific separators, type conversion on read), the CsvHelper package is the standard step up — but for exports, twenty lines of correct code beats a dependency.

DataTable → List<T>

The typed list is the gateway conversion — once you have List<Product>, LINQ, serializers, and APIs all work naturally:

record Product(int Id, string Name, decimal Price);

static List<Product> ToList(DataTable table) =>
    [.. table.Rows.Cast<DataRow>().Select(r =>
        new Product(r.Field<int>("Id"), r.Field<string>("Name")!, r.Field<decimal>("Price")))];

DataRow.Field<T> (from System.Data.DataSetExtensions, built into modern .NET) beats raw indexing because it understands DBNull: r.Field<decimal?>("Price") returns null for database NULLs instead of throwing or handing you a DBNull object to trip over. For nullable columns, make the record property nullable and let Field<T?> do the mapping.

Explicit mapping per column looks like boilerplate until you need reflection-based generic mappers — which fail at runtime on the first type mismatch and cost you the compile-time checking that is the whole point of the typed list. For the handful of shapes a real app converts, explicit wins.

List<Product>: 3 items, first = Product { Id = 1, Name = Mechanical keyboard, Price = 89.00 }

DataTable → JSON

Serialize the typed list, not the DataTable:

var json = JsonSerializer.Serialize(ToList(table),
    new JsonSerializerOptions { WriteIndented = true });
[
  { "Id": 1, "Name": "Mechanical keyboard", "Price": 89.00 },
  { "Id": 2, "Name": "4K "UltraSharp" monitor", "Price": 329.99 },
  { "Id": 3, "Name": "USB-C dock, 11-in-1", "Price": 149.50 }
]

Why the detour through List<T>? System.Text.Json doesn't serialize DataTable out of the box (Newtonsoft did, which is how a lot of accidental API contracts were born). That default is a feature: serializing a DataTable directly couples your JSON shape to database column names forever. The record is your contract — rename a column and the compiler shows you every place to fix, while the JSON stays stable. The " escapes are System.Text.Json being conservative; consumers parse them identically, and JavaScriptEncoder.UnsafeRelaxedJsonEscaping prettifies if humans read the output.

Choosing the target format

Need Convert to
Excel/import file for another system CSV
LINQ, validation, further processing List<T>
API response, message payload, cache entry JSON (via List<T>)
Feeding SqlBulkCopy keep the DataTable — it's the native source

The pattern behind all three: get out of DataTable into typed objects as early as possible, and let every downstream concern work with real types. The complete runnable program is in the companion repository, next to the LINQ DataTable comparison sample that picks up where this one ends.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.