On this page
Convert a JSON Array File to Parquet (Not JSONL)
The situation
An API dump or export tool gave you events.json that looks like this:
[
{"id": 1, "user": {"name": "Ana", "plan": "pro"}, "tags": ["a", "b"], "ts": "2026-10-01T09:12:00Z"},
{"id": 2, "user": {"name": "Ben", "plan": "free"}, "tags": [], "ts": "2026-10-01T09:15:30Z"}
]
It is not JSONL — it is one array — and you want Parquet so it is smaller, typed and queryable. DuckDB handles this without pandas.
Plain array of objects
COPY (
SELECT * FROM read_json('events.json', format = 'array')
) TO 'events.parquet' (FORMAT parquet, COMPRESSION zstd);
read_json samples the records and infers a schema: id becomes BIGINT,
ts becomes TIMESTAMP, user becomes a STRUCT(name VARCHAR, plan VARCHAR)
and tags a VARCHAR[]. Those nested types are written as native Parquet
structs and lists, not as JSON strings. Check what was inferred before
writing:
DESCRIBE SELECT * FROM read_json('events.json', format = 'array');
As a one-liner from a shell:
duckdb -c "COPY (FROM read_json('events.json', format='array')) TO 'events.parquet' (FORMAT parquet)"
Records wrapped in an object
Many APIs return {"count": 2, "data": [ ... ]}. Read the outer object and
unnest the array:
COPY (
SELECT unnest(data, recursive := true)
FROM read_json('response.json', maximum_object_size = 200000000)
) TO 'records.parquet' (FORMAT parquet);
recursive := true expands each record's fields into columns. Drop it if
you prefer one struct column per record.
Errors you will hit
-
"maximum_object_size of 16777216 bytes exceeded" — the whole file is one JSON value (common in the wrapped case). Set
maximum_object_sizeto more than the file size in bytes, as above. For multi-GB arrays, converting to JSONL once withjq -c '.[]' events.json > events.jsonlis faster and streams. -
A column comes out as
JSONorVARCHARinstead of a struct — the field has mixed shapes across records (an object here, a string there), or the variation sits beyond the inference sample. Passsample_size = -1to scan every record, or declare the types yourself:read_json('events.json', format = 'array', columns = {id: 'BIGINT', user: 'STRUCT(name VARCHAR, plan VARCHAR)', tags: 'VARCHAR[]', ts: 'TIMESTAMPTZ'}) -
IDs lose leading zeros or overflow — values such as
"00123"stay strings, but unquoted 20-digit numbers becomeDOUBLE. Declare themVARCHARincolumns. -
Timestamps stay strings — non-ISO formats are not detected. Pass
timestampformat = '%d/%m/%Y %H:%M'or cast withstrptime()in theSELECT.
In the browser
Drop the .json file into the SQL Workbench and run the same
read_json(..., format = 'array') query to check the inferred schema and
spot bad records — the file stays on your machine. The workbench downloads
results as CSV; for a direct Parquet output, convert the array to JSONL and
use JSONL to Parquet, which infers the schema
the same way and writes ZSTD-compressed Parquet locally. Open the result in
the Parquet Viewer to confirm nested columns kept their
structure.
Going the other direction? See Parquet to JSON.
Frequently asked questions
- What is the difference between JSON and JSONL for conversion?
- JSONL has one complete object per line, so it can be read in a stream. A JSON array is a single value wrapped in brackets. DuckDB reads both, but arrays need format = 'array' and very large ones are slower to parse.
- How do I convert only the records inside a key like data or results?
- Read the file as one object, then unnest the array: SELECT unnest(data, recursive := true) FROM read_json('response.json'). Export that query to Parquet with COPY ... TO.
- Why do I get a maximum_object_size error?
- DuckDB limits a single JSON value to 16 MB by default. When the whole file is one object wrapping an array, raise the limit with maximum_object_size set to a value larger than the file size in bytes.
- Can I convert a JSON array without installing anything?
- Yes. Drop the .json file into the browser SQL Workbench and query it with read_json. For simplest results, convert it to JSONL first and use the JSONL to Parquet converter, which runs locally.