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:
{"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:
fx orders.jsonl '.customer'Output:
alice
bob
carol
aliceFilter NDJSON Records
?expr filters the stream. Records that fail the check are dropped, and later arguments run only on the rest:
fx orders.jsonl '?.status == "paid"' '.id'Output:
1001
1003To filter and transform in one expression, return skip for records you want to drop:
fx orders.jsonl '.status == "paid" ? x.id : skip'Output:
1001
1003Keep 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:
fx orders.jsonl '?.total > 20' JSON.stringifyOutput:
{"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:
fx --slurp orders.jsonl . > orders.jsonOutput:
[
{
"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:
fx orders.json '@JSON.stringify(x)' list > orders.jsonlInput:
[
{"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:
echo '{"id":1,"ok":true}{"id":2,"ok":false}{"id":3,"ok":true}' | fx '.id'Output:
1
2
3Aggregate 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:
fx -s orders.jsonl '?.status == "paid"' '@.total' '.reduce((a, b) => a + b, 0)'Output:
142.4Count orders per customer:
fx -s orders.jsonl 'groupBy(x => x.customer)' 'x => Object.fromEntries(Object.entries(x).map(([k, v]) => [k, v.length]))'Output:
{
"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:
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
logoutBrowse a JSONL File Interactively
Open the file without arguments to explore every record in the TUI:
fx orders.jsonlEach 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.