jsonl
A lightweight JSON Lines (JSONL) reader and writer for Rust, with optional async support via Tokio.
Repository Health
Technical Analysis
jsonl is a small, focused Rust crate that implements the JSON Lines format — a simple convention where JSON values are separated by newlines, commonly used for streaming logs, structured data pipelines, and inter-process communication. It exposes read() and write() functions that work directly with any type implementing Serde’s Serialize/DeserializeOwned traits, handling line-buffered reads, EOF detection, and UTF-8-safe serialization automatically.
For situations where a reader and writer need to travel together — such as talking to a child process over stdin/stdout or handling a TCP connection — the crate provides a Connection type that bundles both ends and exposes matching read/write/flush methods. An optional tokio feature swaps the standard-library IO primitives for their async Tokio equivalents, letting the same API work in both synchronous and asynchronous contexts.
What You Get
- Simple read()/write() functions that (de)serialize any Serde-compatible type from/to a JSON Lines stream
- A Connection type that bundles a reader and writer together for stateful, bidirectional protocols
- Built-in constructors for common Connection sources: stdio, child process pipes, and TCP streams
- An optional tokio feature that swaps in async IO primitives without changing the public API shape
- Typed error enums (ReadError, WriteError) built with thiserror distinguishing IO, serialization, and EOF failures
Common Use Cases
- Streaming structured logs or events line-by-line to a file or stdout
- Implementing a line-delimited JSON-RPC-style protocol between a parent process and a spawned child
- Passing structured messages over a TCP socket without a custom framing protocol
- Building command-line tools that consume/produce JSONL for compatibility with jq or other JSON Lines-based pipelines
Under The Hood
Architecture
The crate is tiny and single-purpose: a lib.rs that free-function-exports read() and write(), plus a connection.rs module defining a Connection struct that pairs a reader and writer and delegates to those same free functions for its own read/write/flush methods. The main architectural device is a #[cfg(feature = "tokio")] split — the crate duplicates its core imp module once for synchronous std::io primitives and once for async tokio::io primitives, rather than abstracting over a shared trait, so any change to the core read/write logic has to be made in two nearly-identical places. A separate errors.rs defines the crate’s two error enums. There is no deeper layering — this is a data clump-avoidance utility, not a system with multiple architectural tiers.
Tech Stack Written in Rust (2018 edition) with a minimal dependency set: serde and serde_json for (de)serialization, thiserror for typed error enums, and an optional tokio dependency (io-util, io-std, net, process features) gated behind a Cargo feature flag. There’s no build tooling beyond Cargo itself, no database, and no deployment target — it ships as a published crate on crates.io with docs hosted on docs.rs.
Code Quality No test files or #[cfg(test)] modules exist anywhere in the repository, and there is no CI configuration, so the crate ships with zero automated verification of its behavior. That said, the code itself is clean: error handling is explicit and typed throughout (ReadError/WriteError distinguish IO, deserialize, and EOF failures rather than swallowing them), naming is idiomatic Rust, and the API is strongly generic over any Serialize/DeserializeOwned type rather than hardcoding to a specific struct. Every public item carries a doc comment, which partially offsets the total absence of tests.
API Design The public surface is deliberately small — two free functions (read, write) plus one helper type (Connection) with a handful of named constructors for its most common instantiations (stdio, child-process pipes, TCP streams). This keeps the boilerplate to reach a working read/write loop near zero: callers pass any BufRead/Write implementor and get automatic (de)serialization. The tradeoff is that swapping between sync and async requires recompiling with a different Cargo feature rather than choosing at runtime, and the crate offers no framing/length-prefixing alternative if a consumer’s protocol isn’t strictly line-delimited.