JSON and JSON Lines (JSONL) are the default formats for API responses, event streaming (Kafka/Kinesis), and telemetry logs. However, using raw JSON for analytical querying (OLAP) at scale leads to severe performance degradation and skyrocketing cloud costs.
Because JSON is a row-oriented, text-based format, analytical engines like AWS Athena, Google BigQuery, Snowflake, and DuckDB must parse every single line, byte, and unused column from disk into memory.
Apache Parquet is an open-source, columnar storage file format optimized for fast data processing and analytical queries. Converting large JSON datasets to Parquet frequently yields an 80–90% reduction in storage size and speeds up analytical queries by 10x–50x.
1. Architectural Comparison: Row-Oriented JSON vs. Columnar Parquet
ROW-ORIENTED (JSON / JSONL):
┌─────────────────────────────────────────────────────────────┐
│ Row 1: { id: 101, user: "Alice", status: "OK", amount: 50 } │
│ Row 2: { id: 102, user: "Bob", status: "FAIL", amount: 20 }│
│ Row 3: { id: 103, user: "Carol", status: "OK", amount: 90 } │
└─────────────────────────────────────────────────────────────┘
* Query: "SELECT SUM(amount) WHERE status = 'OK'"
* Disk I/O: Must read 100% of all bytes across all columns.
COLUMNAR (APACHE PARQUET):
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Column: id │ Column: user │Column: status│Column: amount│
│ [101, 102, │ ["Alice", │ ["OK", │ [50, 20, 90] │
│ 103] │ "Bob", │ "FAIL", │ (Snappy/ZSTD)│
│ │ "Carol"] │ "OK"] │ │
└──────────────┴──────────────┴──────────────┴──────────────┘
* Query: "SELECT SUM(amount) WHERE status = 'OK'"
* Disk I/O: Reads ONLY 'status' and 'amount' columns. Skips 50%+ of bytes!
Key Performance Benefits of Parquet
| Dimension | JSON / JSONL | Apache Parquet |
|---|---|---|
| Storage Layout | Row-oriented | Column-oriented (Row Groups & Pages) |
| Compression | Generic gzip (moderate ratio) | Column-specific (Dictionary, RLE, Snappy, ZSTD) |
| Schema | Implicit, untyped | Explicit, strongly typed binary schema |
| Projection Pushdown | Read all fields, filter in CPU | Read only requested columns from disk |
| Predicate Pushdown | Scan every record sequentially | Skips row groups via Min/Max column statistics |
| Cloud Query Cost | High (Billed for full file scan) | Low (Billed only for columns read) |
2. Converting JSON to Parquet in Python
Using DuckDB (Fastest & Zero-Configuration)
DuckDB provides high-performance schema inference and streaming conversion without loading the entire JSON payload into RAM.
import duckdb
# Stream JSON / JSONL directly to compressed Parquet with auto-schema inference
duckdb.sql("""
COPY (
SELECT * FROM read_json_auto('events.jsonl')
) TO 'events.parquet' (
FORMAT 'parquet',
CODEC 'zstd',
COMPRESSION_LEVEL 3
);
""")
print("Successfully converted events.jsonl to events.parquet with ZSTD compression.")
Using PyArrow & Pandas
When building programmatic ETL pipelines:
import json
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
# Sample nested JSON events
raw_json = """
[
{"event_id": "e-100", "user_id": 401, "action": "checkout", "price": 49.99, "timestamp": "2026-09-04T10:00:00Z"},
{"event_id": "e-101", "user_id": 402, "action": "add_to_cart", "price": 12.50, "timestamp": "2026-09-04T10:05:00Z"},
{"event_id": "e-102", "user_id": 401, "action": "checkout", "price": 99.00, "timestamp": "2026-09-04T10:15:00Z"}
]
"""
# Load into DataFrame
df = pd.read_json(raw_json)
df['timestamp'] = pd.to_datetime(df['timestamp'])
# Convert to PyArrow Table with explicit typing
table = pa.Table.from_pandas(df)
# Write Parquet with Snappy compression and dictionary encoding
pq.write_table(
table,
'analytics_events.parquet',
compression='SNAPPY',
use_dictionary=['action'],
row_group_size=50000
)
3. Converting JSON to Parquet in Node.js
In JavaScript and TypeScript backend microservices, use parquetjs-lite:
import parquet from 'parquetjs-lite';
// 1. Define explicit Parquet schema
const schema = new parquet.ParquetSchema({
userId: { type: 'INT64' },
email: { type: 'UTF8', compression: 'SNAPPY' },
country: { type: 'UTF8', encoding: 'DICTIONARY' },
signupDate: { type: 'TIMESTAMP_MILLIS' },
metrics: {
optional: true,
fields: {
loginCount: { type: 'INT32' },
lastActive: { type: 'TIMESTAMP_MILLIS' }
}
}
});
async function writeParquetFile() {
const writer = await parquet.ParquetWriter.openFile(schema, 'users.parquet');
await writer.appendRow({
userId: 1001n,
email: '[email protected]',
country: 'US',
signupDate: new Date(),
metrics: { loginCount: 14, lastActive: new Date() }
});
await writer.close();
}
4. Production Best Practices for Parquet Pipelines
- Choose the Right Compression Codec:
- Snappy: Default for low-latency streaming and real-time ingestion. Balances fast CPU decompression with 60–75% compression.
- Zstandard (ZSTD): Recommended for analytical data lakes, cold storage, and AWS Athena. Delivers superior compression ratios (80–90%) with fast decompression.
- Row Group Sizing: Aim for row groups between 128 MB and 512 MB. Very small row groups prevent effective dictionary encoding and degrade predicate pushdown efficiency.
- Partitioning Strategy: Partition data in object storage by date or high-cardinality keys (e.g.,
s3://bucket/events/year=2026/month=09/day=04/data.parquet). This enables cloud engines to skip scanning unrelated partitions entirely.
Tip: Instantly inspect and convert ad-hoc JSON files into binary Parquet files directly in your browser using the DevFlow JSON to Parquet Converter.