---
title: "What is NDJSON / JSON Lines?"
date: "2026-06-08"
author: "Al Brown"
category: "Engineering"
excerpt: "NDJSON (newline-delimited JSON), also called JSON Lines, stores one JSON object per line. This page explains the format, how it differs from a single JSON array, and proves it by reading a real file with clickhouse local."
---

# What is NDJSON / JSON Lines?

**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`](/resources/engineering/what-is-clickhouse-local) at it with `FORMAT JSONEachRow`:


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

## At a glance

| Property | NDJSON / JSON Lines |
|---|---|
| Layout | Row-oriented text, one JSON object per line |
| Delimiter | Newline (`\n`) between records; no commas, no wrapping `[ ]` |
| Schema | None enforced; inferred from the JSON keys and value types |
| Streamable | Yes — each line is parsed independently |
| Compression | None built in; wraps cleanly with `.gz` / `.zst` |
| File extension | `.ndjson`, `.jsonl`, sometimes `.json` |
| ClickHouse FORMAT | `JSONEachRow` |
| Typical use | Logs, 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:


```text
{"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:


```json
[
  {"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](/resources/engineering/run-sql-on-csv-file) is more compact for flat, tabular data but cannot represent nesting. [Parquet](/resources/engineering/what-is-parquet-file) 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](/resources/engineering/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`](/resources/engineering/what-is-clickhouse-local) is part of ClickHouse; install it with `clickhousectl`, the ClickHouse CLI for local and cloud:


```bash
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:


```bash
clickhouse local -q "DESCRIBE file('events.ndjson', JSONEachRow)"
```

```response
   ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
   ┃ 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:


```bash
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"
```

```response
   ┏━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┓
   ┃ 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:


```bash
clickhouse local -q "
SELECT country, count() AS events
FROM file('events_array.json', JSONEachRow)
GROUP BY country
ORDER BY country"
```

```response
   ┏━━━━━━━━━┳━━━━━━━━┓
   ┃ 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:


```bash
head -n 100 events_large.ndjson | clickhouse local -q "
SELECT count() AS rows, max(revenue) AS max_revenue
FROM file('-', JSONEachRow)"
```

```response
   ┏━━━━━━┳━━━━━━━━━━━━━┓
   ┃ 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`](/resources/engineering/what-is-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](https://clickhouse.com/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](/resources/engineering/run-sql-on-jsonl-file) and [query an NDJSON file](/resources/engineering/query-ndjson-file).

**Run it yourself:** the data generator and every command above live in [`local-analytics/what-is-ndjson`](https://github.com/ClickHouse/examples/tree/main/local-analytics/what-is-ndjson) in the ClickHouse examples repo.

Prefer Python? → [Read a JSONL file in Python](/resources/engineering/read-jsonl-file-python).
