Every language
12 languages, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
import { appendFileSync, createReadStream } from 'node:fs';
import { createInterface } from 'node:readline/promises';
// write: one JSON.stringify per record — it escapes any embedded newline
appendFileSync('events.jsonl', JSON.stringify({ id: 1, msg: 'multi\nline' }) + '\n');
// read: readline + one JSON.parse per line — never parse the whole file
const rl = createInterface({ input: createReadStream('events.jsonl') });
for await (const line of rl) {
if (!line.trim()) continue; // blank line: decided, skipped
const rec = JSON.parse(line);
}JSON.stringify turns an embedded newline into the two bytes \n — the line framing survives. Hand-assembly (msg + '|' + ts) writes the raw byte and silently corrupts every reader after it. The interface also yields a final line with no trailing \n.
TSTypeScript
import { createReadStream, createWriteStream } from 'node:fs';
import { createInterface } from 'node:readline/promises';
// write: append loop — each record serialized on its own line
export async function writeJsonl(path: string, rows: unknown[]): Promise<void> {
const out = createWriteStream(path, { flags: 'a' });
for (const row of rows) out.write(JSON.stringify(row) + '\n');
await new Promise<void>((res, rej) => out.end((err) => (err ? rej(err) : res())));
}
// read: AsyncGenerator yields one parsed record at a time
export async function* readJsonl<T>(path: string): AsyncGenerator<T> {
const rl = createInterface({ input: createReadStream(path) });
for await (const line of rl) {
if (line.trim()) yield JSON.parse(line) as T;
}
}The generator keeps memory flat — a 10 GB file streams the same as a 10 KB one because nothing accumulates. T is a claim, not a check: the cast trusts the file, so validate records at the boundary when the file is foreign.
GoGo
import (
"bufio"
"bytes"
"encoding/json"
"os"
)
type Record struct {
ID int `json:"id"`
Msg string `json:"msg"`
}
// write: one compact Marshal per record, one line each
func appendJSONL(path string, recs []Record) error {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
w := bufio.NewWriter(f)
for _, r := range recs {
b, err := json.Marshal(r) // compact: no raw newlines inside
if err != nil {
return err
}
w.Write(b)
w.WriteByte('\n')
}
return w.Flush()
}
// read: scanner lines, one Unmarshal per line, record per callback
func readJSONL(path string, fn func(Record) error) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 64*1024), 8*1024*1024) // raise the 64KB line cap
for sc.Scan() {
line := bytes.TrimSpace(sc.Bytes())
if len(line) == 0 {
continue // blank line: decided, skipped
}
var r Record
if err := json.Unmarshal(line, &r); err != nil {
return err
}
if err := fn(r); err != nil {
return err
}
}
return sc.Err()
}json.Marshal escapes <, >, & to \u003c… by default (HTML escaping) — still valid JSON, but raw grepping surprises; an Encoder with SetEscapeHTML(false) writes them plain when that matters. And Scanner's default line cap is 64 KB: one long minified record ends the loop with bufio.ErrTooLong unless Buffer() raises it.
RsRust
use serde::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, Write};
#[derive(Serialize, Deserialize)]
struct Record {
id: u32,
msg: String,
}
fn append_jsonl(path: &str, recs: &[Record]) -> std::io::Result<()> {
let mut f = OpenOptions::new().create(true).append(true).open(path)?;
for r in recs {
serde_json::to_writer(&mut f, r)?; // compact — no newlines inside
f.write_all(b"\n")?;
}
Ok(())
}
fn read_jsonl(path: &str) -> std::io::Result<Vec<Record>> {
let mut out = Vec::new();
for line in BufReader::new(OpenOptions::new().read(true).open(path)?).lines() {
let line = line?;
if line.trim().is_empty() {
continue; // blank line: decided, skipped
}
out.push(serde_json::from_str(&line).map_err(std::io::Error::other)?);
}
Ok(out)
}The ndjson ecosystem crate exists, but this 10-line hand-roll IS the idiomatic Rust — to_writer is already compact and lines() is already the framing. serde errors map through io::Error::other because the lines() iterator only speaks io::Result.
PHPPHP
// write: 'ab' = append + binary-safe; one json_encode per row
$f = fopen('events.jsonl', 'ab');
foreach ($rows as $row) {
// JSON_UNESCAPED_UNICODE keeps real UTF-8 bytes instead of \uXXXX escapes
fwrite($f, json_encode($row, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n");
}
fclose($f);
// read: fgets line loop; JSON_THROW_ON_ERROR makes failure loud
$f = fopen('events.jsonl', 'rb');
while (($line = fgets($f)) !== false) {
$line = trim($line);
if ($line === '') continue; // blank line fails json_decode — guard it
$row = json_decode($line, true, 512, JSON_THROW_ON_ERROR);
}
fclose($f);Blank lines fail json_decode — it returns null, which is ALSO a valid decoded value, so without JSON_THROW_ON_ERROR the failure is silent. The everyday malformed line is the blank one a stray double newline leaves behind: trim and skip.
PyPython
import json
# write: one dumps per row — compact by default, one line per record
with open('events.jsonl', 'a', encoding='utf-8') as f:
for row in rows:
f.write(json.dumps(row) + '\n')
# read: the 3-liner — iterate, skip blanks, parse
with open('events.jsonl', encoding='utf-8') as f:
records = [json.loads(line) for line in f if line.strip()]json.dumps escapes embedded newlines to \n, so the line framing is safe by construction. `for line in f` is lazy — a big file streams without slurping. The jsonlines package exists, but this loop is the standard.
C#C#
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
record Row(int Id, string Msg);
// one static options pair — reflection metadata caches attach to the INSTANCE
static readonly JsonSerializerOptions Json = new(); // default IS compact
static void WriteJsonl(string path, IEnumerable<Row> rows)
{
using var writer = new StreamWriter(path, append: true);
foreach (var row in rows)
{
writer.WriteLine(JsonSerializer.Serialize(row, Json)); // one line each
}
}
static IEnumerable<Row> ReadJsonl(string path) =>
File.ReadLines(path) // lazy — streams the file
.Where(l => !string.IsNullOrWhiteSpace(l))
.Select(l => JsonSerializer.Deserialize<Row>(l, Json)!);File.ReadLines is the lazy one — ReadAllLines materializes a string[] of every line first. And REUSE one JsonSerializerOptions instance: the first (de)serialize with a given instance builds and caches metadata — a fresh options per row re-pays that every line.
JvJava
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
class Jsonl {
// ONE static mapper — thread-safe after config, expensive to build
private static final ObjectMapper MAPPER = new ObjectMapper();
record Row(int id, String msg) {}
static void write(Path path, List<Row> rows) throws IOException {
try (var out = Files.newBufferedWriter(path,
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
for (Row row : rows) {
out.write(MAPPER.writeValueAsString(row)); // compact
out.write('\n');
}
}
}
static List<Row> read(Path path) throws IOException {
try (BufferedReader in = Files.newBufferedReader(path)) {
return in.lines()
.filter(s -> !s.isBlank())
.map(Jsonl::readRow) // checked IOException → wrap for the stream
.toList();
}
}
private static Row readRow(String line) {
try {
return MAPPER.readValue(line, Row.class);
} catch (IOException e) {
throw new UncheckedIOException(line, e); // the offending line is the context
}
}
}A single ObjectMapper instance is thread-safe once configured — build it once (static), never per row. readValue throws the checked IOException, which a stream lambda cannot propagate; UncheckedIOException carries the offending line out with it.
SwSwift
import Foundation
struct Row: Codable { let id: Int; let msg: String }
// write: default JSONEncoder IS compact — set no formatting options
func appendJsonl(_ rows: [Row], to url: URL) throws {
let encoder = JSONEncoder()
let handle = try FileHandle(forWritingTo: url)
defer { try? handle.close() }
_ = try handle.seekToEnd() // append, don't clobber
for row in rows {
try handle.write(contentsOf: encoder.encode(row))
try handle.write(contentsOf: Data("\n".utf8)) // the separator
}
}
// read: split into lines, decode each — split drops empties by default
func readJsonl(_ url: URL) throws -> [Row] {
let decoder = JSONDecoder()
let text = try String(contentsOf: url, encoding: .utf8)
return try text.split(separator: "\n")
.map { try decoder.decode(Row.self, from: Data($0.utf8)) }
}Compact output requires no formatting options — the default JSONEncoder IS compact; .prettyPrinted would break the framing (encode through singleValueContainer() when a custom Encodable insists on pretty). String(contentsOf:) loads the whole file — fine for configs, use a buffered line reader for multi-GB logs.
KtKotlin
import com.fasterxml.jackson.databind.ObjectMapper
import java.io.File
data class Row(val id: Int, val msg: String)
private val mapper = ObjectMapper() // one instance, reused — same JVM rule as Java
// write: appendText per record + '\n' — the one-line append
fun writeJsonl(path: String, rows: List<Row>) {
rows.forEach { row ->
File(path).appendText(mapper.writeValueAsString(row) + "\n")
}
}
// read: lazy sequence — records parse as you iterate, memory stays flat
fun readJsonl(path: String) = sequence {
File(path).bufferedReader().use { reader ->
while (true) {
val line = reader.readLine() ?: break
if (line.isNotBlank()) yield(mapper.readValue(line, Row::class.java))
}
}
}appendText + '\n' per record is the one-liner append — it reopens the file each call, fine for log-style writes, hoist a BufferedWriter for hot loops. The sequence {} builder reads lazily so a huge file stays flat; yield must be called directly in the builder body — a regular lambda (forEach on a materialized list) cannot contain it.
RbRuby
require 'json'
# write: JSON.generate — compact, exactly one line per record
File.open('events.jsonl', 'a') do |f|
rows.each { |row| f.puts JSON.generate(row) }
end
# read: each_line + one JSON.parse per line
File.open('events.jsonl').each_line do |line|
next if line.strip.empty? # blank line: decided, skipped
row = JSON.parse(line)
endJSON.pretty_generate would BREAK the format — a pretty record spans many physical lines, so the one-line-per-record rule dies. JSON.generate (or to_json) is always compact; puts supplies the separator.
ZigZig
const std = @import("std");
const Row = struct { id: u32, msg: []const u8 };
// write: one minified Stringify per record + the '\n' separator
pub fn writeJsonl(path: []const u8, rows: []const Row) !void {
const file = try std.fs.cwd().createFile(path, .{ .truncate = false });
defer file.close();
try file.seekFromEnd(0);
var bw = std.io.bufferedWriter(file.writer());
const w = bw.writer();
for (rows) |row| {
try std.json.Stringify.value(row, .{}, w); // compact by default
try w.writeAll("\n");
}
try bw.flush();
}
// read: line iterator + one parse per line, arena reset between records
pub fn readJsonl(gpa: std.mem.Allocator, path: []const u8) !void {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
var br = std.io.bufferedReader(file.reader());
const r = br.reader();
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
var line_buf: [64 * 1024]u8 = undefined; // cap on ONE record
while (try r.readUntilDelimiterOrEof(&line_buf, '\n')) |line| {
_ = arena.reset(.retain_capacity); // free the previous record
const row = try std.json.parseFromSliceLeaky(Row, arena.allocator(), line, .{});
std.debug.print("{d} {s}\n", .{ row.id, row.msg });
}
}The arena-per-line pattern keeps memory flat: parseFromSliceLeaky parks every allocation of one record in the arena, and reset(.retain_capacity) reclaims it all before the next line — no per-field frees, no growth. Stringify.value is minified by default; the 64 KB line buffer is the record cap (StreamTooLong past it).