Skip to content

Working With NDJSON and JSON Lines Files ​

NDJSON (newline-delimited JSON) and JSON Lines (.jsonl) store one JSON value per line. Exports, event streams and logs use them. Install fx and it streams these files natively, one record at a time, with no extra flags.

The examples use this orders.jsonl file:

json
{"id": 1001, "customer": "alice", "total": 42.5, "status": "paid"}
{"id": 1002, "customer": "bob", "total": 18, "status": "pending"}
{"id": 1003, "customer": "carol", "total": 99.9, "status": "paid"}
{"id": 1004, "customer": "alice", "total": 7.25, "status": "refunded"}

Process a JSON Lines File Line by Line ​

Each argument runs once per line, and every result is printed on its own:

bash
fx orders.jsonl '.customer'

Output:

alice
bob
carol
alice

Filter NDJSON Records ​

?expr filters the stream. Records that fail the check are dropped, and later arguments run only on the rest:

bash
fx orders.jsonl '?.status == "paid"' '.id'

Output:

1001
1003

To filter and transform in one expression, return skip for records you want to drop:

bash
fx orders.jsonl '.status == "paid" ? x.id : skip'

Output:

1001
1003

Keep Output in JSON Lines Format ​

By default fx pretty-prints each result. Add JSON.stringify as the last argument to get one compact line per record, so the output is still valid JSONL:

bash
fx orders.jsonl '?.total > 20' JSON.stringify

Output:

{"id":1001,"customer":"alice","total":42.5,"status":"paid"}
{"id":1003,"customer":"carol","total":99.9,"status":"paid"}

Convert JSONL to a JSON Array ​

Use --slurp (-s) to read all lines into one array:

bash
fx --slurp orders.jsonl . > orders.json

Output:

json
[
  {
    "id": 1001,
    "customer": "alice",
    "total": 42.5,
    "status": "paid"
  },
  {
    "id": 1002,
    "customer": "bob",
    "total": 18,
    "status": "pending"
  },
  {
    "id": 1003,
    "customer": "carol",
    "total": 99.9,
    "status": "paid"
  },
  {
    "id": 1004,
    "customer": "alice",
    "total": 7.25,
    "status": "refunded"
  }
]

Convert a JSON Array to JSONL ​

Stringify each element with @, then print the array one item per line with list:

bash
fx orders.json '@JSON.stringify(x)' list > orders.jsonl

Input:

json
[
  {"id": 1001, "customer": "alice", "total": 42.5},
  {"id": 1002, "customer": "bob", "total": 18},
  {"id": 1003, "customer": "carol", "total": 99.9}
]

Output:

{"id":1001,"customer":"alice","total":42.5}
{"id":1002,"customer":"bob","total":18}
{"id":1003,"customer":"carol","total":99.9}

Without list, fx prints the array itself as JSON.

Read Concatenated JSON Without Newlines ​

fx also handles JSON values written back to back, with no separator between them:

bash
echo '{"id":1,"ok":true}{"id":2,"ok":false}{"id":3,"ok":true}' | fx '.id'

Output:

1
2
3

Aggregate Over a Whole JSONL File ​

After --slurp the input is an array, so ?, @, and built-ins like groupBy and sortBy work across all records. Sum the paid orders:

bash
fx -s orders.jsonl '?.status == "paid"' '@.total' '.reduce((a, b) => a + b, 0)'

Output:

142.4

Count orders per customer:

bash
fx -s orders.jsonl 'groupBy(x => x.customer)' 'x => Object.fromEntries(Object.entries(x).map(([k, v]) => [k, v.length]))'

Output:

json
{
  "alice": 2,
  "bob": 1,
  "carol": 1
}

WARNING

--slurp loads every record into memory before running your code. On very large files, first filter line by line without -s, then slurp only the smaller result: fx big.jsonl '?.status == "paid"' JSON.stringify | fx -s '@.total' '.reduce((a, b) => a + b, 0)'.

Skip Invalid Lines in a JSONL File ​

A line that is not valid JSON stops fx with an error:

Unexpected character 'o' on line 2.

  not json
  .^

To ignore bad lines, read the input as raw strings with -r and parse each line yourself:

bash
fx -r mixed.jsonl 'x => { try { return JSON.parse(x).event } catch { return skip } }'

Input:

{"id": 1, "event": "login"}
not json
{"id": 2, "event": "logout"}

Output:

login
logout

Browse a JSONL File Interactively ​

Open the file without arguments to explore every record in the TUI:

bash
fx orders.jsonl

Each line is shown as its own value. Collapse and expand records with the arrow keys or Vim keys, press / to search with a regex (n/N for next and previous match), @ to fuzzy-search paths, and P to print the selected value to stdout. See key bindings.

See Also ​