<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Med Chour</title>
    <description>The latest articles on DEV Community by Med Chour (@med-chour).</description>
    <link>https://dev.to/med-chour</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4236221%2F15aa62bc-ce4e-4be3-a1e2-0ae6323c830e.jpg</url>
      <title>DEV Community: Med Chour</title>
      <link>https://dev.to/med-chour</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/med-chour"/>
    <language>en</language>
    <item>
      <title>How to Flatten Nested JSON into a CSV Table</title>
      <dc:creator>Med Chour</dc:creator>
      <pubDate>Tue, 10 Nov 2026 04:57:00 +0000</pubDate>
      <link>https://dev.to/med-chour/how-to-flatten-nested-json-into-a-csv-table-jh9</link>
      <guid>https://dev.to/med-chour/how-to-flatten-nested-json-into-a-csv-table-jh9</guid>
      <description>&lt;p&gt;API responses rarely look like a spreadsheet. An order contains a customer object, the customer contains an address, and the order contains a list of items. CSV, on the other hand, is a flat grid: one row per record, one value per cell. Turning one into the other means deciding what to do with every nested object and every array. There is no single right answer, so it helps to know the options.&lt;/p&gt;

&lt;h2&gt;
  
  
  The example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Leila"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Fes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"30000"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"A1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"B7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Tom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Lyon"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"A1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"coupon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"WELCOME"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Strategy 1: Nested objects become dot-notation columns
&lt;/h2&gt;

&lt;p&gt;Each nested key is joined to its parent with a dot. &lt;code&gt;customer.address.city&lt;/code&gt; becomes a column name, and the value goes in that column. Objects map perfectly to this approach because each object has exactly one value per key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,customer.name,customer.address.city,customer.address.zip,paid,coupon
1001,Leila,Fes,30000,true,
1002,Tom,Lyon,,false,WELCOME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter here. First, the column list must be built from &lt;strong&gt;all&lt;/strong&gt; records, not just the first one — otherwise &lt;code&gt;coupon&lt;/code&gt;, which only appears in the second order, disappears. Second, missing values become empty cells rather than shifting the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 2: Arrays become JSON text in one cell
&lt;/h2&gt;

&lt;p&gt;Arrays are harder: an order may have one item or fifty. The simplest lossless option is to keep the array as JSON text in a single cell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,customer.name,items
1001,Leila,"[{""sku"":""A1"",""qty"":2},{""sku"":""B7"",""qty"":1}]"
1002,Tom,"[{""sku"":""A1"",""qty"":5}]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is lost and the number of columns stays fixed, but the items are not individually sortable or filterable in a spreadsheet. This is what the &lt;a href="https://dataswitchpro.com/json-to-csv/" rel="noopener noreferrer"&gt;JSON to CSV converter&lt;/a&gt; does by default, combined with dot notation for objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 3: Explode arrays into one row per element
&lt;/h2&gt;

&lt;p&gt;When the array holds the data you want to analyse — order lines, transactions, events — create one row per element and repeat the parent fields on each row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,customer.name,sku,qty
1001,Leila,A1,2
1001,Leila,B7,1
1002,Tom,A1,5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the right shape for pivot tables and SQL imports: you can now sum quantities per SKU. The trade-off is duplication, and you can only explode &lt;em&gt;one&lt;/em&gt; array per table — exploding two independent arrays at once multiplies rows into a meaningless cross product. If a record has several arrays, create one table per array, linked by the parent ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 4: Numbered columns
&lt;/h2&gt;

&lt;p&gt;Some tools write arrays as &lt;code&gt;items.0.sku&lt;/code&gt;, &lt;code&gt;items.1.sku&lt;/code&gt; and so on. It works for short arrays with a known maximum length, such as a list of up to three phone numbers, but it produces wide, sparse tables for anything longer and the column count changes with the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a strategy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Just need everything in Excel?&lt;/strong&gt; Dot notation plus JSON text cells (strategies 1 and 2). Fast and lossless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analysing the contents of an array?&lt;/strong&gt; Explode it (strategy 3), one table per array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loading into a database?&lt;/strong&gt; Explode arrays into separate tables with a foreign key — the same idea as strategy 3, normalised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrays of a few fixed items?&lt;/strong&gt; Numbered columns can be acceptable (strategy 4).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Doing it in Python
&lt;/h2&gt;

&lt;p&gt;pandas' &lt;code&gt;json_normalize&lt;/code&gt; covers strategies 1 and 3. Without &lt;code&gt;record_path&lt;/code&gt;, it flattens objects with dots; with it, it explodes an array and copies the parent fields you list in &lt;code&gt;meta&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"orders.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Strategy 1: one row per order, nested objects as dot columns&lt;/span&gt;
&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json_normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"orders.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Strategy 3: one row per item, with order fields repeated&lt;/span&gt;
&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json_normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;record_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"order_items.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pitfalls to watch for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inconsistent records:&lt;/strong&gt; fields that appear in only some objects must still get a column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keys containing dots:&lt;/strong&gt; a key like &lt;code&gt;"v1.2"&lt;/code&gt; becomes ambiguous after flattening. Rename such keys first if you need to convert back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large integers:&lt;/strong&gt; IDs longer than 15–16 digits lose precision in JavaScript and spreadsheets. Keep them as strings in the JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commas and quotes in values:&lt;/strong&gt; always write CSV with a proper library or converter so these are quoted correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Flatten objects with dot-notation column names, keep arrays as JSON text when you need a quick lossless export, and explode arrays into separate rows or tables when you need to analyse them. Try the &lt;a href="https://dataswitchpro.com/json-to-csv/" rel="noopener noreferrer"&gt;JSON to CSV converter&lt;/a&gt; on your own data to see the default approach in action, or use &lt;a href="https://dataswitchpro.com/json-to-excel/" rel="noopener noreferrer"&gt;JSON to Excel&lt;/a&gt; to get a workbook directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can a flattened CSV be converted back to the original JSON?&lt;/strong&gt;&lt;br&gt;
Dot-notation columns can be turned back into nested objects, and JSON text cells can be parsed again. Exploded rows need to be regrouped by the parent ID. The round trip is easiest when keys contain no dots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just use one column per array element?&lt;/strong&gt;&lt;br&gt;
It works for short, fixed-length arrays, but for variable lists it creates very wide, mostly empty tables whose columns change whenever the data does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about deeply nested JSON?&lt;/strong&gt;&lt;br&gt;
Dot notation handles any depth for objects. For deeply nested arrays, explode one level at a time into separate tables.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dataswitchpro.com/guides/flatten-nested-json-to-csv/" rel="noopener noreferrer"&gt;DataSwitch Pro&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>json</category>
      <category>csv</category>
      <category>python</category>
      <category>data</category>
    </item>
  </channel>
</rss>
