Skip to content

What is NDJSON / JSON Lines?

Al Brown
Last updated: Jun 8, 2026

NDJSON (newline-delimited JSON), also called JSON Lines or JSONL, is a text format that stores one independent JSON object per line, separated by newlines. There is no enclosing array and no commas between records. Because each line is a complete, self-contained document, a program can read, write, and process the file one line at a time without holding the whole thing in memory. To query an NDJSON file in place, point clickhouse local at it with FORMAT JSONEachRow:

clickhouse local -q "SELECT * FROM file('events.ndjson', JSONEachRow)"

At a glance

PropertyNDJSON / JSON Lines
LayoutRow-oriented text, one JSON object per line
DelimiterNewline (\n) between records; no commas, no wrapping [ ]
SchemaNone enforced; inferred from the JSON keys and value types
StreamableYes — each line is parsed independently
CompressionNone built in; wraps cleanly with .gz / .zst
File extension.ndjson, .jsonl, sometimes .json
ClickHouse FORMATJSONEachRow
Typical useLogs, event streams, API exports, append-only data

NDJSON vs a single JSON array

The whole format is one rule: one JSON object, one line. A three-record NDJSON file looks like this:

{"id":0,"event_time":"2026-01-01 00:00:00","country":"GB","event_type":"view","revenue":367.42}
{"id":1,"event_time":"2026-01-01 00:01:00","country":"AU","event_type":"click","revenue":7.53}
{"id":2,"event_time":"2026-01-01 00:02:00","country":"IN","event_type":"purchase","revenue":103.67}

The same three records as a single JSON array look like this:

[
  {"id":0,"event_time":"2026-01-01 00:00:00","country":"GB","event_type":"view","revenue":367.42},
  {"id":1,"event_time":"2026-01-01 00:01:00","country":"AU","event_type":"click","revenue":7.53},
  {"id":2,"event_time":"2026-01-01 00:02:00","country":"IN","event_type":"purchase","revenue":103.67}
]

Same data, two different documents. The difference between them comes down to streamability.

A JSON array is a single document. The opening [ and the closing ] belong together, and the records are joined by commas. To parse it correctly you generally have to read to the closing bracket, because the file is only valid JSON as a whole. Appending a record means rewriting the end of the file.

NDJSON has no enclosing structure. Each line stands alone, so you can read one record, process it, and discard it before touching the next. You can tail -f an NDJSON file as it grows, split it across machines on line boundaries, or append a new record by writing one more line. That is why logs, event pipelines, and bulk-export APIs almost always emit NDJSON rather than a giant array.

How it relates to JSON, CSV and Parquet

NDJSON is still JSON: each line carries its own keys, so the data is self-describing per record, and nested objects and arrays are allowed inside a line. That flexibility is the trade-off. There is no shared schema, types live in the values rather than a header, and the keys repeat on every line, so NDJSON files are larger and slower to scan than a typed columnar format.

CSV is more compact for flat, tabular data but cannot represent nesting. Parquet is columnar, typed and compressed, which makes analytical queries far faster, at the cost of being a binary format you cannot read or append by hand. A common pattern is to land data as NDJSON and convert it to Parquet for analysis; see convert NDJSON to Parquet.

Reading a real NDJSON file

Description is one thing; reading a real file is more convincing. ClickHouse calls the NDJSON format JSONEachRow, and it infers the schema directly from the JSON in each line. clickhouse local is part of ClickHouse; install it with clickhousectl, the ClickHouse CLI for local and cloud:

curl https://clickhouse.com/cli | sh   # install clickhousectl
clickhousectl local use latest         # download ClickHouse and put it on your PATH

Point DESCRIBE at the file to see the schema ClickHouse infers from the JSON values, with no header and no type hints:

clickhouse local -q "DESCRIBE file('events.ndjson', JSONEachRow)"
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
   ┃ name       ┃ type               ┃
   ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
1. │ id         │ Nullable(Int64)    │
2. │ event_time │ Nullable(DateTime) │
3. │ country    │ Nullable(String)   │
4. │ event_type │ Nullable(String)   │
5. │ revenue    │ Nullable(Float64)  │
   └────────────┴────────────────────┘

ClickHouse read the keys as column names and inferred each type from the values: id is an integer, event_time is parsed as a DateTime, revenue is a float. Now query it like any table, no load step:

clickhouse local -q "
SELECT country, count() AS events, round(sum(revenue)) AS revenue
FROM file('events.ndjson', JSONEachRow)
GROUP BY country
ORDER BY revenue DESC"
┏━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┓
   ┃ country ┃ events ┃ revenue ┃
   ┡━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━┩
1. │ GB      │      2 │     558 │
2. │ IN      │      2 │     547 │
3. │ DE      │      1 │     449 │
4. │ AU      │      2 │     374 │
5. │ US      │      1 │      36 │
   └─────────┴────────┴─────────┘

JSONEachRow is forgiving about the wrapper. The same parser also reads a top-level JSON array, so if someone hands you the array form instead, the identical query still works:

clickhouse local -q "
SELECT country, count() AS events
FROM file('events_array.json', JSONEachRow)
GROUP BY country
ORDER BY country"
┏━━━━━━━━━┳━━━━━━━━┓
   ┃ country ┃ events ┃
   ┡━━━━━━━━━╇━━━━━━━━┩
1. │ AU      │      2 │
2. │ DE      │      1 │
3. │ GB      │      2 │
4. │ IN      │      2 │
5. │ US      │      1 │
   └─────────┴────────┘

Proving streamability

The defining property of NDJSON is that you can process it a line at a time. Pipe just the first 100 lines of a 1.5-million-row file into ClickHouse and it parses cleanly, because each line is a complete record:

head -n 100 events_large.ndjson | clickhouse local -q "
SELECT count() AS rows, max(revenue) AS max_revenue
FROM file('-', JSONEachRow)"
┏━━━━━━┳━━━━━━━━━━━━━┓
   ┃ rows ┃ max_revenue ┃
   ┡━━━━━━╇━━━━━━━━━━━━━┩
1. │  100 │      499.84 │
   └──────┴─────────────┘

Try that with a JSON array and head hands you a truncated, invalid document with no closing bracket. NDJSON does not care where you cut it.

Scanning the whole file is fast too. Filtering and aggregating all 1,500,000 rows of the NDJSON file (147 MB) runs in 0.34s (best of three, warm cache, on an Apple M4 Pro, 14 cores, 24 GB RAM; the machine had other load at the time). JSON text parsing is more work than reading a typed binary format, so if you query the same data repeatedly, convert it to Parquet first.

Read one yourself

You don't need a server or an import step to work with NDJSON. clickhouse local reads a .ndjson, .jsonl or line-delimited .json file in place from the command line; it's a handy single-binary tool for files on your laptop. The same SQL runs unchanged against a file, a ClickHouse server, or ClickHouse Cloud, so a query you write against a local NDJSON file scales up without edits.

For the full walkthrough, covering globbing many files, extracting nested fields and casting types, see run SQL on a JSONL file and query an NDJSON file.

Run it yourself: the data generator and every command above live in local-analytics/what-is-ndjson in the ClickHouse examples repo.

Prefer Python? → Read a JSONL file in Python.


Share this resource

  • Y Combinator icon
  • X icon
  • Bluesky icon
  • Facebook icon
  • LinkedIn icon

Subscribe to our newsletter

Stay informed on feature releases, product roadmap, support, and cloud offerings!

More like this

AlloyDB alternatives compared for 2026: Aurora, RDS, Neon, Crunchy Bridge, and ClickHouse Managed Postgres with benchmarks, tradeoffs, and decision criteria.

Continue reading ->

ClickHouse concurrency: how to size for user-facing analytics

Manveer Chawla • Last updated: Jul 2, 2026

How to size ClickHouse for high-concurrency, user-facing analytics: turn active users into query load, benchmark under production-like conditions, and configure per-query limits, admission controls, workload scheduling, and replicas.

Continue reading ->

When to denormalize, when to join: A ClickHouse guide

Manveer Chawla • Last updated: Jun 26, 2026

Denormalization and normalization are both valid analytical data-modeling strategies. A decision framework for choosing where to denormalize, where to join, and which ClickHouse primitives bridge the gap.

Continue reading ->