Flatten JSON
How to flatten nested JSON into columns
Drop a JSON or JSONL file up to 1.5 GB onto rows.page, free with no signup, and nested objects up to 8 levels deep become dot-path columns in your browser. The file stays on your machine unless you click Share.
No file handy? Open Every country, as nested JSON (250 rows, 1.4 MB).
What flattening nested JSON means
Flattening turns nested JSON into a flat table: each record becomes one row, and each nested key becomes a column named by its path from the root, joined with dots. Take this record:
{
"id": 101,
"user": {
"name": "Ada",
"address": {
"city": "Utrecht"
}
},
"tags": ["sql", "json"]
}| Input field | Flattened column | Example value |
|---|---|---|
id | id | 101 |
name inside user | user.name | Ada |
city inside user.address | user.address.city | Utrecht |
tags (array) | tags (list column) | ["sql", "json"] |
Objects flatten cleanly because each key holds one value. Arrays hold several, so they stay a list column until you explode them into one row per item.
How rows.page flattens JSON on open
Open or drop a .json, .jsonl or .ndjson file, or click Paste data, and DuckDB-WASM flattens it inside your browser tab. The grid has no row limit and loads 100 rows at a time as you scroll.
- Nested objects become dot-path columns such as
user.address.city, up to 8 levels deep and 200 child fields per object. A deeper or wider object stays whole in one column. - Arrays stay list columns instead of becoming
tags.0andtags.1; the grid shows the first 6 items and+N. - Click a row to open it in a side drawer with a JSON/Fields toggle and Copy as JSON. There is no whole-file tree view.
- A
.jsonfile over 1.5 GB is refused ("Too big for this browser"). A JSONL file over 1.5 GB loads its first rows with the note "Loaded the first N rows: the file is X, more than the browser can hold."
Explode JSON arrays into rows with SQL unnest()
To get one row per array item, type SQL into the filter box above the table. One statement starting with SELECT, WITH or FROM runs as DuckDB SQL against a table called data:
SELECT id, "user.address.city", unnest(tags) AS tag FROM data- Quote dot-path names:
"user.address.city". Unquoted, DuckDB readsuseras a table and fails withReferenced table "user" not found!. - To keep every column, write
SELECT * EXCLUDE (__rid, tags), unnest(tags) AS tag FROM data.__ridis a hidden row-number column that*would otherwise include. - For an array of objects such as
items: [{"sku": "A1", "qty": 2}],unnest(items)returns one struct column, shown as JSON text.unnest(items, recursive := true)splits it intoskuandqtycolumns. - Results stop at the first 1,000,000 rows ("first 1M").
Export flattened JSON to CSV or Parquet
The Export menu saves the current view: filters, sort, hidden columns or your SQL result. Filtered views export every matching row; SQL results stop at 1,000,000 rows.
- CSV: comma-delimited, header row, UTF-8 without a BOM. Lists become JSON text such as
["sql","json"], and cells starting with=,+,-,@, a tab or a carriage return get a leading apostrophe so spreadsheets don't run them as formulas. - Parquet: zstd compression.
- JSON (one array of objects) or JSONL (one object per line): keys keep their dot paths, such as
"user.name", and lists stay arrays. - Markdown: a table of the first 1,000 rows, with lists written as JSON text.
There is no delimiter, encoding or BOM option, and no Excel .xlsx export.
Flatten JSON in Python pandas and DuckDB CLI
For a pipeline, or a file over 1.5 GB, flatten on your own machine. pandas json_normalize flattens every level, with no depth or field limit:
import json
import pandas as pd
with open("in.json") as f:
records = json.load(f)
df = pd.json_normalize(records, sep=".")
df["tags"] = df["tags"].map(json.dumps)
df.to_csv("out.csv", index=False)Without the json.dumps line, to_csv writes lists as Python text such as ['sql', 'json'], which is not valid JSON. For a wrapped file like {"data": [...]}, pass record_path="data"; for JSONL, build records with [json.loads(line) for line in f if line.strip()]. pandas holds the whole file in memory.
COPY (
SELECT id, unnest(user, recursive := true), tags
FROM read_json('in.json')
) TO 'flat.parquet';The DuckDB CLI reads the file from disk, and recursive := true flattens every object inside user in one pass. Columns take the last key (name, city), not the dot path, so a second name key comes out as name_1, and lists inside stay lists. For dot-path names, list each field: user.address.city AS "user.address.city".
Common pitfalls when flattening JSON
- Late keys: DuckDB's
read_jsonpicks columns from the first 20,480 rows by default, so a key that first appears later is dropped without an error. Addsample_size = -1to scan the whole file. rows.page scans files up to 50 MB in full; in bigger files a late key can be missing. - Key collisions: a literal
"user.name"key and a nesteduserobject with anamekey flatten to the same name. rows.page keeps both and renames oneuser.name (2); pandas keeps a singleuser.namecolumn and silently drops one value. - Empty arrays:
unnest(tags)drops rows whose list is empty or null. UnnestCASE WHEN len(tags) > 0 THEN tags ELSE [NULL] ENDinstead to keep them with a nulltag.
rows.page limits and prices
| What | Limit or price |
|---|---|
| Open a file in your browser | Files up to 1.5 GB, $0, no account, nothing uploaded |
| Share a link without an account | $0, files up to 100 MB, link deleted 7 days after creation |
| Free account | $0, 10 datasets kept, last 3 versions |
| Pro | $12/month, 100 datasets, files up to 500 MB, private links |
| Max | $39/month, 1,000 datasets, files up to 2 GB, private links |
Questions
Is my JSON file uploaded to a server when I flatten it on rows.page?
No. rows.page reads the file inside your browser with DuckDB-WASM, and nothing leaves your computer unless you press Share. Share sends one upload of up to 100 MB on every plan, and the link expires after 7 days unless you sign in with Google and press Keep it.
Can I flatten a JSON file hosted at a remote URL without downloading it first?
Yes. Put the public URL after https://rows.page/, as in https://rows.page/https://example.com/data.json. Your browser fetches it directly, or through the rows.page proxy if the host blocks cross-origin requests (up to 100 MB, 30 seconds, 5 redirects), and nothing is stored.
What happens if my JSON file is a single object with a data array inside it?
rows.page unwraps a single top-level object such as {"data": [...]} to its longest list, up to 4 levels deep, and notes Showing the N items in "data"; the other top-level fields were skipped.
Can rows.page flatten JSON strings stored inside a CSV or Parquet column?
Yes. When 90% or more of the values in the first 1,000 rows of a text column are JSON objects, rows.page expands it into columns such as payload.city and notes Expanded the JSON in "payload" into N columns. The raw text column stays hidden but queryable, and a column that would expand into more than 200 columns is left as text.
Can I push a JSON file from the terminal and get a shareable link?
Yes: curl -T data.json https://rows.page uploads it and prints a link. Each request takes up to 100 MB on every plan, and without an API key the link expires after 7 days. Free keeps 10 hosted datasets, Pro ($12 per month) 100 and Max ($39 per month) 1,000; Pro and Max send files up to 500 MB and 2 GB through multipart uploads at POST /api/v1/uploads.