๐ Format Conversion ยท 8 min read
How Do I Convert a CSV to Nested JSON?
A CSV holds one flat record per line with no nesting in the format, so nested JSON is produced by grouping, not by a conversion switch: you choose which column identifies the parent, then pack the repeating columns under it as an array.
Every attempt that skips that decision ends the same way. There is no converter setting for "make this nested", because the file does not say what the parent is. The order export below has four rows, two of which belong to the same order, and only a human reading the header can tell that order_id is the thing being described and the item columns are the things being listed.
Quick answer
Group the rows by the column that repeats, build one object per group, put the shared columns in the parent and the row-level columns in a child array. In Python that is a sort plus itertools.groupby or df.groupby; the whole job is about fifteen lines. If the nesting you need is only one level deep, columns named customer.name and customer.email give you the path without any grouping at all.
Why the file format forbids nesting
RFC 4180, section 2 describes the format in five lines: each record is on its own line, records are delimited by a line break, a record contains one or more fields separated by commas, and each line should contain the same number of fields throughout the file. Fields containing commas, double quotes or line breaks have to be enclosed in double quotes, and a quote inside a quoted field is escaped by doubling it.
Nothing in that description can hold a name. A field is text between delimiters; it does not have members. The nesting on the other side is real: RFC 8259 describes an object as an unordered collection of name/value pairs and an array as an ordered sequence of values, and a value in either can itself be an object or an array. So the structure you want exists in the JSON data model and has no counterpart in the CSV one. The conversion has to add it.
Three different things people mean by "nested"
The phrase covers at least three target shapes, and they need different work. Pick yours before writing code, because the last two are the ones that go wrong.
| Target shape | What changes | Work required |
|---|---|---|
| A sub-object inside the record | {"customer": {"name": ...}} | split the column names on a prefix, then nest one level |
| A parent with a child array | {"order_id": 1001, "items": [...]} | group rows by the parent key, then collect the children |
| A wrapper around the whole file | {"meta": ..., "rows": [...]} | a header object plus the flat array, no grouping |
The middle one is where most "nested" requests land, and it is the only one that can lose rows. A group of three becomes one object, so the output has fewer top-level entries than the input had lines, and anything that counted rows will now count orders.
The file we will convert
Four rows, two orders, one repeated customer per order. This is the smallest file that actually needs grouping.
order_id,customer_name,customer_email,item_sku,qty
1001,Ann Lee,ann@example.com,SKU-1,2
1001,Ann Lee,ann@example.com,SKU-2,1
1002,Bo Chen,bo@example.com,SKU-3,5
Grouping in the standard library
Open the file with newline="" so the csv module handles the line breaks itself, sort by the parent key, then let groupby hand you one group at a time. The output below is the real output of this script on the file above, not a sketch.
import csv, json
from itertools import groupby
with open("orders.csv", newline="") as f:
rows = list(csv.DictReader(f))
rows.sort(key=lambda r: r["order_id"])
out = []
for order_id, group in groupby(rows, key=lambda r: r["order_id"]):
group = list(group)
first = group[0]
out.append({
"order_id": int(order_id),
"customer": {"name": first["customer_name"], "email": first["customer_email"]},
"items": [{"sku": r["item_sku"], "qty": int(r["qty"])} for r in group],
})
print(json.dumps(out, indent=2))
[
{
"order_id": 1001,
"customer": {
"name": "Ann Lee",
"email": "ann@example.com"
},
"items": [
{ "sku": "SKU-1", "qty": 2 },
{ "sku": "SKU-2", "qty": 1 }
]
},
{
"order_id": 1002,
"customer": { "name": "Bo Chen", "email": "bo@example.com" },
"items": [
{ "sku": "SKU-3", "qty": 5 }
]
}
]
Two details in that code are load-bearing. The sort is not optional: the Python docs say the iterable given to groupby generally needs to be sorted on the same key function, and without it a key that appears, disappears and reappears yields two groups. The int() calls are also deliberate, because DictReader hands you strings and JSON would otherwise carry "qty": "2".
The same grouping in pandas
pandas does not need the sort, and the group key arrives as a separate variable rather than as an attribute of the frame. Measured on pandas 2.3.3.
import json
import pandas as pd
df = pd.read_csv("orders.csv")
def pack(order_id, g):
return {
"order_id": int(order_id),
"customer": {"name": g.customer_name.iloc[0], "email": g.customer_email.iloc[0]},
"items": g[["item_sku", "qty"]].rename(columns={"item_sku": "sku"}).to_dict("records"),
}
nested = [pack(oid, g) for oid, g in df.groupby("order_id")]
print(json.dumps(nested, indent=2))
to_dict("records") is the useful piece: it returns one dict per row, which is exactly the child array you want, with column names already acting as keys. The .iloc[0] is how the shared columns are lifted out, since every row of a group repeats them.
Going back: what flattening does to that document
Nested JSON that has to become a spreadsheet again goes through flattening, andpandas.json_normalize is the usual tool. Its output on the JSON above, with record_path="items" and the parent columns passed as meta, is a normal table:
sku qty order_id customer.name
0 SKU-1 2 1001 Ann Lee
1 SKU-2 1 1001 Ann Lee
2 SKU-3 5 1002 Bo Chen
Without record_path the same document flattens differently: the nested object becomes a dot-notation column, but the array stays one object in one cell, printed as a Python list. That is the same trap as quoting in CSV files in a different costume: the cell holds text that looks like structure and is not. Any round trip through CSV therefore needs the nesting to be rebuilt from a rule, not read back off the file.
Doing the simple version in the browser
When each row really is one record, and the only nesting you need is a level of grouping on a prefix, our JSON / CSV converter covers the mechanical part: it turns each CSV row into one JSON object, recovers numbers and booleans from the text, and on the JSON to CSV side flattens nested keys into dot-notation columns. It does not decide that order_id is your parent, and it should not: a tool that picks a parent by guessing produces keys nobody asked for, which is worse than a flat array you can see is wrong.
For the flat cases, the honest shortcut is column naming. A header of customer.name, customer.email converts to JSON with those exact keys, and those keys are the path you would have to build by hand anyway. If you need the real parent-child shape, group in code, and use the CSV analyzer first to confirm that the column you plan to group on is actually unique per parent, rather than mostly unique.
Related reading: CSV to JSON for the flat case, JSON to CSV for the reverse direction, and CSV vs JSON for which format suits which job.
Frequently asked questions
Can a CSV file be nested?
No. RFC 4180 section 2 says each record sits on its own line and contains one or more fields separated by commas, and that each line should contain the same number of fields throughout the file. That is a rectangle: rows, fields, one level. There is no syntax in the format for a field that contains a set of named children. A cell can hold JSON text, but that is a string the reader has to parse, not structure the CSV carries.
How do I convert a CSV to nested JSON?
Choose the column that identifies a parent record, group all rows sharing that value, then build one object per group: the shared columns go in the parent object, the columns that differ row by row become an array of child objects. In Python this is a sort followed by itertools.groupby, or df.groupby in pandas. If your target nesting is only one level deep, naming the columns customer.name and customer.email and then nesting on the dot is enough.
Which column should become the parent object?
The one whose value repeats across rows while the other columns change. In an order export, order_id identifies the order, and the item columns differ per row, so order_id is the parent and the item rows become the array. If no column repeats, the file has no parent-child relationship in it and one object per row is the correct output, which is what a plain CSV to JSON conversion already gives you.
Why does my nested data show up as [object Object] in a CSV?
That message comes from the other direction, JSON into CSV. A CSV cell holds text, so a converter that writes an object into a cell falls back to the language default: JavaScript writes [object Object], Python writes the dict repr. The fix is to flatten before writing, which is what pandas.json_normalize does with a separator such as a dot, then write the resulting columns. Nothing recovers the nesting automatically when the file is read back.
Does row order matter when grouping?
Yes, if you use itertools.groupby. The Python documentation states that the iterable generally needs to be sorted on the same key function, and unsorted input produces a new group each time the key reappears: two rows with order_id 1001 either side of 1002 give two separate groups instead of one. Sort by the parent key first, or use a dict keyed by the parent value, which does not care about order. pandas groupby does not require sorted input.
How do I keep numbers as numbers in the JSON?
CSV has no types, so a reader gives you strings and you convert them explicitly, for example int(qty) or float(price). Doing it explicitly is safer than guessing, because an identifier like 007 is text that looks numeric and a value like 1,234 is text that looks broken. If a column is an identifier, leave it as a string even when every value is digits.
Can I get nested JSON without writing code?
A browser converter can reshape one row into one object, which covers the case where each row is already a complete record. It cannot decide on your behalf which repeated column is the parent, and guessing wrong is worse than not guessing: it produces keys nobody asked for. When there is a real parent-child relationship in the file, the grouping is a data decision and belongs in code or in a tool that lets you name the key.
Check the Columns Before You Group
Paste the first rows and confirm which column repeats per record before you collapse rows into objects.
The record, field, quoting and field-count wording is from RFC 4180, section 2; the object and array definitions are from RFC 8259, section 1 and section 4. The grouping requirement for itertools.groupby is stated in the Python standard library documentation for itertools, and the flattening parameters (record_path, meta) come from the pandas json_normalize documentation. Both code listings were run on 2026-11-02 with Python 3.13.5 and pandas 2.3.3 on the four-row file shown above; the JSON and table output in this article is copied from those runs.
Related reading
Format Conversion โ other guides that pair well with this one.
Browse all guides in the NoCodeCSV blog.