How to convert JSON to CSV across languages
Real snippets to export JSON to CSV across languages. Useful for reporting, handoff, and pipelines where rows and columns are still the expected format.
Use this article when...
This section is built for speed: one concrete task, multiple stacks, and the minimum context needed not to copy the wrong thing.
Work from the request or response body that actually failed instead of rebuilding JSON from memory.
The useful path here is fast adaptation: choose your stack, then copy only after reading the notes around parsing and formatting.
Once the snippet runs, inspect the live payload with JSON Formatter or the related tool to confirm the structure is really fixed.
What this article gives you
A short path from incident to implementation: context, language-specific snippets, and the warning signs that usually get skipped.
13
3
json, csv, export, data, conversion
JSON is great for systems and APIs, but many operational reviews, manual exports, and spreadsheet-heavy processes still expect CSV. Conversion is useful when the goal is no longer preserving nesting, but producing a reviewable table.
The important decision here is not technical but semantic: which columns you export, how you serialize nested objects, and what happens when some rows have keys others do not.
Dependency snapshot by language
Check this first to see which snippets use built-in/runtime APIs and which ones need external packages before you start copying code.
Built-in or runtime API
CsvHelper.
Built-in or runtime API
Built-in or runtime API
Review the notes for setup details.
csv-stringify.
Review the notes for setup details.
Built-in or runtime API
Built-in or runtime API
Built-in or runtime API
Review the notes for setup details.
csv-stringify.
Built-in or runtime API
How to use these code examples well
The snippet is only the fast path. The explanation and notes are what keep you from copying the wrong assumption into production.
Make sure the article really matches the issue you are debugging before copying code just because the title looks close.
That is where portability limits, dependency requirements, and runtime assumptions usually appear.
If the code runs but the bug remains, jump to the related tool and inspect the real payload, token, timestamp, or config.
Code examples by language
Snippets that compile/run, plus short explanations and real-world notes.
Bash
python3 - <<'PY'
import csv
import io
import json
rows = json.loads('[{"id":1,"name":"Ana","active":true},{"id":2,"name":"Luis","active":false}]')
buffer = io.StringIO()
writer = csv.DictWriter(buffer, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
print(buffer.getvalue(), end='')
PY
If you need this quickly from the terminal, Python handles JSON parsing and CSV export with the standard library and without rewriting escaping logic.
- Standard library only.
- Assumes all rows share the same base columns.
C#
using System.Globalization;
using System.Text.Json;
using CsvHelper;
static string ToCell(JsonElement value) => value.ValueKind switch
{
JsonValueKind.String => value.GetString() ?? string.Empty,
JsonValueKind.Number => value.GetRawText(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
JsonValueKind.Null => string.Empty,
_ => value.GetRawText()
};
var input = """
[
{"id":1,"name":"Ana","active":true},
{"id":2,"name":"Luis","active":false}
]
""";
var rows = JsonSerializer.Deserialize<List<Dictionary<string, JsonElement>>>(input)!;
var headers = rows[0].Keys.ToList();
using var writer = new StringWriter();
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
foreach (var header in headers)
{
csv.WriteField(header);
}
csv.NextRecord();
foreach (var row in rows)
{
foreach (var header in headers)
{
csv.WriteField(row.TryGetValue(header, out var value) ? ToCell(value) : string.Empty);
}
csv.NextRecord();
}
Console.WriteLine(writer.ToString());
JsonElement lets you decide how each type is exported without losing control, and CsvHelper takes care of correct CSV escaping.
- Dependency: CsvHelper.
- For optional columns, build the union of keys before writing the header.
Delphi
program JsonToCsv;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
System.Classes,
System.JSON,
System.StrUtils;
function EscapeCsv(const S: string): string;
begin
if ContainsText(S, ',') or ContainsText(S, '"') or ContainsText(S, sLineBreak) then
Result := '"' + StringReplace(S, '"', '""', [rfReplaceAll]) + '"'
else
Result := S;
end;
function JsonValueToCell(const Obj: TJSONObject; const Name: string): string;
var
V: TJSONValue;
begin
V := Obj.Values[Name];
if V = nil then
Exit('');
if V is TJSONString then
Exit(TJSONString(V).Value);
Result := V.Value;
end;
var
JsonArray: TJSONArray;
Headers: TStringList;
Pair: TJSONPair;
I: Integer;
Row: TJSONObject;
begin
JsonArray := TJSONObject.ParseJSONValue('[{"id":1,"name":"Ana","active":true},{"id":2,"name":"Luis","active":false}]') as TJSONArray;
Headers := TStringList.Create;
try
for Pair in JsonArray.Items[0] as TJSONObject do
Headers.Add(Pair.JsonString.Value);
Writeln(String.Join(',', Headers.ToStringArray));
for I := 0 to JsonArray.Count - 1 do
begin
Row := JsonArray.Items[I] as TJSONObject;
Writeln(
EscapeCsv(JsonValueToCell(Row, Headers[0])) + ',' +
EscapeCsv(JsonValueToCell(Row, Headers[1])) + ',' +
EscapeCsv(JsonValueToCell(Row, Headers[2]))
);
end;
finally
Headers.Free;
JsonArray.Free;
end;
end.
With System.JSON you can read the array, take keys from the first row, and write a simple CSV with explicit escaping.
- Standard library only.
- For more complex exports, move headers and escaping into reusable helpers.
Go
package main
import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
)
func main() {
input := "[{"id":1,"name":"Ana","active":true},{"id":2,"name":"Luis","active":false}]"
var rows []map[string]any
if err := json.Unmarshal([]byte(input), &rows); err != nil {
panic(err)
}
headers := []string{"id", "name", "active"}
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
if err := writer.Write(headers); err != nil {
panic(err)
}
for _, row := range rows {
record := []string{
fmt.Sprint(row["id"]),
fmt.Sprint(row["name"]),
fmt.Sprint(row["active"]),
}
if err := writer.Write(record); err != nil {
panic(err)
}
}
}
In Go it is usually better to define the columns you want to export explicitly. That avoids depending on map order and forces you to think about the CSV contract.
- Standard library only.
- If nested objects exist, convert them to JSON strings first or flatten them deliberately.
Java
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class Main {
public static void main(String[] args) throws Exception {
String input = """
[
{"id":1,"name":"Ana","active":true},
{"id":2,"name":"Luis","active":false}
]
""";
ObjectMapper mapper = new ObjectMapper();
List<Map<String, Object>> rows = mapper.readValue(input, new TypeReference<List<Map<String, Object>>>() {});
List<String> headers = new ArrayList<>(rows.get(0).keySet());
StringWriter out = new StringWriter();
try (CSVPrinter printer = new CSVPrinter(out, CSVFormat.DEFAULT.builder().setHeader(headers.toArray(String[]::new)).build())) {
for (Map<String, Object> row : rows) {
List<String> record = new ArrayList<>();
for (String header : headers) {
record.add(Objects.toString(row.get(header), ""));
}
printer.printRecord(record);
}
}
System.out.println(out);
}
}
Jackson parses the JSON and Apache Commons CSV writes properly escaped CSV so you do not have to compose rows by hand.
- Dependencies: jackson-databind and commons-csv.
- Declare the column order if you want the export to stay stable across runs.
JavaScript
import { stringify } from 'csv-stringify/sync';
const rows = [
{ id: 1, name: 'Ana', active: true },
{ id: 2, name: 'Luis', active: false }
];
const csv = stringify(rows, {
header: true,
columns: ['id', 'name', 'active']
});
console.log(csv);
csv-stringify avoids manual mistakes with quotes, commas, and line breaks. If you already export from Node, it is a very reasonable dependency.
- Dependency: csv-stringify.
- Define columns explicitly so the output stays stable.
Kotlin
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVPrinter
import java.io.StringWriter
fun main() {
val input = """
[
{"id":1,"name":"Ana","active":true},
{"id":2,"name":"Luis","active":false}
]
""".trimIndent()
val mapper = jacksonObjectMapper()
val rows: List<Map<String, Any?>> = mapper.readValue(input, object : TypeReference<List<Map<String, Any?>>>() {})
val headers = rows.first().keys.toList()
val out = StringWriter()
CSVPrinter(out, CSVFormat.DEFAULT.builder().setHeader(*headers.toTypedArray()).build()).use { printer ->
rows.forEach { row ->
printer.printRecord(headers.map { header -> row[header]?.toString() ?: "" })
}
}
println(out.toString())
}
Kotlin/JVM can rely on Jackson for parsing and Commons CSV to produce a stable, properly escaped export.
- Dependencies: jackson-module-kotlin and commons-csv.
- If nested values exist, decide up front whether to flatten or serialize them.
PHP
<?php
$json = '[{"id":1,"name":"Ana","active":true},{"id":2,"name":"Luis","active":false}]';
$rows = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
$handle = fopen('php://temp', 'r+');
fputcsv($handle, array_keys($rows[0]));
foreach ($rows as $row) {
fputcsv($handle, $row);
}
rewind($handle);
echo stream_get_contents($handle);
fclose($handle);
fputcsv handles proper escaping for you. It is the simple path for backoffice exports and operational tasks in PHP.
- Standard functions only.
- If some rows have different keys, build the full list of columns before iterating.
Python
import csv
import io
import json
rows = json.loads('[{"id":1,"name":"Ana","active":true},{"id":2,"name":"Luis","active":false}]')
buffer = io.StringIO()
writer = csv.DictWriter(buffer, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
print(buffer.getvalue(), end='')
csv.DictWriter is enough for the common homogeneous array-of-objects case and saves you from reimplementing CSV escaping.
- Standard library only.
- For optional columns use extrasaction or define fieldnames manually.
Ruby
require 'json'
require 'csv'
rows = JSON.parse('[{"id":1,"name":"Ana","active":true},{"id":2,"name":"Luis","active":false}]')
headers = rows.first.keys
puts CSV.generate do |csv|
csv << headers
rows.each do |row|
csv << headers.map { |header| row[header] }
end
end
Ruby lets you combine JSON.parse and CSV.generate with very little code. It is a clean option for small export scripts.
- Standard library only.
- Keeping headers separate helps preserve stable column order.
Rust
use csv::Writer;
use serde_json::{Map, Value};
fn main() {
let input = r#"[{"id":1,"name":"Ana","active":true},{"id":2,"name":"Luis","active":false}]"#;
let rows: Vec<Map<String, Value>> = serde_json::from_str(input).unwrap();
let headers = vec!["id", "name", "active"];
let mut writer = Writer::from_writer(vec![]);
writer.write_record(&headers).unwrap();
for row in rows {
let record: Vec<String> = headers
.iter()
.map(|header| row.get(*header).map(value_to_cell).unwrap_or_default())
.collect();
writer.write_record(record).unwrap();
}
print!("{}", String::from_utf8(writer.into_inner().unwrap()).unwrap());
}
fn value_to_cell(value: &Value) -> String {
match value {
Value::String(v) => v.clone(),
Value::Null => String::new(),
_ => value.to_string(),
}
}
The csv crate writes properly escaped output and serde_json lets you decide how each value should be serialized before export.
- Dependencies: csv = "1", serde_json = "1".
- Fixing headers explicitly avoids surprises with ordering.
TypeScript
import { stringify } from 'csv-stringify/sync';
type Row = {
id: number;
name: string;
active: boolean;
};
const rows: Row[] = [
{ id: 1, name: 'Ana', active: true },
{ id: 2, name: 'Luis', active: false }
];
const csv = stringify(rows, {
header: true,
columns: ['id', 'name', 'active']
});
console.log(csv);
In TypeScript the added value is making the object shape explicit before exporting. That reduces mistakes when CSV becomes a contract with other teams.
- Dependency: csv-stringify.
- If the real object contains nested values, decide first whether to flatten them or serialize them as JSON.
VB.NET
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.Text.Json
Module Program
Private Function EscapeCsv(value As String) As String
If value.Contains(",") OrElse value.Contains(""") OrElse value.Contains(Environment.NewLine) Then
Return """" & value.Replace("""", """""") & """"
End If
Return value
End Function
Private Function JsonToCell(value As JsonElement) As String
Select Case value.ValueKind
Case JsonValueKind.String
Return value.GetString()
Case JsonValueKind.Null
Return String.Empty
Case Else
Return value.GetRawText()
End Select
End Function
Sub Main()
Dim input = "[{""id"":1,""name"":""Ana"",""active"":true},{""id"":2,""name"":""Luis"",""active"":false}]"
Dim rows = JsonSerializer.Deserialize(Of List(Of Dictionary(Of String, JsonElement)))(input)
Dim headers = rows(0).Keys.ToArray()
Console.WriteLine(String.Join(",", headers))
For Each row In rows
Dim record = headers.Select(Function(header) EscapeCsv(If(row.ContainsKey(header), JsonToCell(row(header)), String.Empty)))
Console.WriteLine(String.Join(",", record))
Next
End Sub
End Module
In VB.NET you can parse JSON with System.Text.Json and write CSV while controlling cell escaping with a small helper function.
- Only the .NET/VB runtime.
- For optional or nested columns, normalize the model before exporting.
Related tools
Use the tool when you need to generate or validate output fast.
Trust and privacy
Copy/paste-friendly by design. Still, treat real production secrets carefully.
- Prefer synthetic examples when sharing screens or recording.
- Do not paste real API keys, tokens, or personal data unless you fully trust your environment.
- If you need ordering, do not use UUID v4 as a sortable id: add createdAt or use sortable ids.