Loading exports¶
iq --jsonl writes one JSON document per line (JSON Lines), the format every
dataframe tool reads directly.
Normalization is what makes that read predictable:
- One canonical rendering per value
- Exact integers
- Decimal strings
- RFC3339Nano UTC timestamps1
- Base64 binary
- An explicit
nullfor an absent field rather than a placeholder.
This section is the consumer's side: loading an export into a data-science stack without losing that fidelity. The whys below were checked against pandas 3.0, Polars 1.42, and DuckDB 1.5.
pandas¶
Pass dtype_backend="pyarrow", not the default. pandas' default NumPy dtypes
have no nullable integer. As a result, the first null in an integer column
silently widens the whole column to float64. Then 7 becomes 7.0, and any
exact integer past 2^53 is corrupted before you look.
The Arrow-backed path keeps a typed, null-safe int64[pyarrow] (missing values
read as pd.NA, not NaN). Decimal strings and RFC3339Nano stay strings that
you can lift to exact types:
import pyarrow as pa
df["amount"] = df["amount"].astype(pd.ArrowDtype(pa.decimal128(20, 2))) # exact decimal
df["ts"] = df["ts"].astype("timestamp[ns, tz=UTC][pyarrow]") # nanosecond UTC
pandas still labels pd.NA semantics experimental. Prefer the Arrow backend, but pin your
pandas version rather than depend on the exact behaviour.
Polars¶
import polars as pl
df = pl.read_ndjson("dump.jsonl")
df.null_count() # O(1) per column, tracked in the validity bitmap
Polars has one missing value, null, uniform across every type. NaN is a float value, not
missingness. iq never emits NaN for an absent field, so mean, min, and null_count stay
correct on iq output. A gap is a null that statistics skip, never a NaN that changes the result.
DuckDB¶
read_json_auto (aka read_json) infers a type per field and shreds nested documents into
STRUCT/LIST you query with dot- and list-access. Two edges of iq's output to know:
- Timestamps need an explicit cast. DuckDB's type sniffer accepts fractional seconds only to
millisecond precision. As a result, iq's nanosecond RFC3339Nano strings (
2026-07-18T12:34:56.123456789Z) are inferred asVARCHAR, notTIMESTAMP(observed on DuckDB 1.5.4, verify in your version). Cast toTIMESTAMP_NSto keep the nanoseconds. PlainTIMESTAMPtruncates to microseconds:
- The UNNEST trap.
UNNESTof an empty ornulllist yields zero rows. As a result, a naiveSELECT id, UNNEST(tags) …silently drops every record whose list is empty or null. Keep them with a lateral left join back:
SELECT j.id, u.tag
FROM read_json_auto('dump.jsonl') j
LEFT JOIN LATERAL UNNEST(j.tags) AS u(tag) ON true;
Splink (entity resolution)¶
Splink needs these inputs:
- A per-record
unique_id - Column names that conform across the sources you link
- Dates truncated to
yyyy-mm-dd - Critically, true nulls, never empty-string placeholders.
iq's export already fits. The key rides in each record (a ready unique_id), and iq emits an
explicit null for an absent field. Prepare a source for Splink with the jq filter, which reshapes,
re-keys, and truncates dates in one pass:
For an entity-resolution consumer, the choice between omitting a field and writing an explicit
null is significant. They are different inputs to the match. Export explicit nulls (a jq object
constructor like {name} already writes null for a missing field). See
Null vs missing for how iq draws that line at each layer.
-
RFC 3339 is the date and time format for use in Internet protocols, a profile of ISO 8601. RFC3339Nano is Go's layout for it with nanosecond precision, so every timestamp renders the same way in every export. https://datatracker.ietf.org/doc/html/rfc3339 ↩