(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisbon

How to parse JSONL files in ETL: step by step

João Barros 02 de October de 2026 4 min read

This tutorial shows how to parse JSONL files in ETL and transform JSON records per line into a tabular format useful for analysis. It is useful when data arrives in JSONL (logs, events) and you need to normalize and load into a database or CSV.

Prerequisites

  • Python 3.8+ installed
  • Libraries: pandas, fastjsonschema (optional for validation)
  • Sample JSONL file (each line is a JSON object)
  • Text editor or IDE and command line

Step 1: understand the JSONL format and define the goal

JSONL (JSON Lines) has one JSON record per line. First, open a few lines to inspect simple and nested fields. Decide which columns you want to extract and whether you need to normalize arrays or nested objects.

Step 2: read JSONL in streaming to avoid running out of memory

For large files read in streaming line by line and process in chunks. This avoids loading everything into memory. This example reads 10,000 lines per chunk and converts to a DataFrame for batch operations.

import json
import pandas as pd
from itertools import islice

def read_jsonl_in_chunks(path, chunk_size=10000):
    with open(path, 'r', encoding='utf-8') as f:
        while True:
            lines = list(islice(f, chunk_size))
            if not lines:
                break
            yield [json.loads(l) for l in lines]

Step 3: normalize nested fields and arrays

Use pandas.json_normalize to flatten nested objects. For arrays, decide whether to explode (one row per element) or aggregate (concatenate/count). In the example below we normalize "user" fields and explode the "events" array.

from pandas import json_normalize

def process_chunk(objs):
    # transform list of dicts into a flat DataFrame
    df = json_normalize(objs)
    # example: user.id -> user.id, user.name -> user.name
    # handle events array: each record may have an events list
    if 'events' in df.columns:
        df_events = df.explode('events')
        # events is now dict or NaN
        events_df = json_normalize(df_events['events'].dropna())
        # reindex to align with df_events
        events_df.index = df_events.index[:len(events_df)]
        df_events = df_events.drop(columns=['events']).join(events_df)
        return df_events
    return df

Step 4: validation and handling common errors

Common errors: malformed lines, fields in unexpected formats and null values. Validate each line with try/except and log failures for later analysis. You can use fastjsonschema to validate the structure before processing.

import logging
logging.basicConfig(level=logging.INFO)

def safe_load(line):
    try:
        return json.loads(line)
    except json.JSONDecodeError as e:
        logging.warning(f'Linha inválida: {e}')
        return None

# example usage with streaming
for chunk in read_jsonl_in_chunks('data.jsonl'):
    objs = [o for o in chunk if o is not None]
    df = process_chunk(objs)
    # continue with transformations / persistence

Step 5: normalize types, handle timestamps and primary keys

Convert timestamps to datetime, normalize strings and generate a consistent primary key (hash) if ids do not exist. This helps with incremental loading and deduplication.

import hashlib

def ensure_types(df):
    if 'timestamp' in df.columns:
        df['timestamp'] = pd.to_datetime(df['timestamp'], errors='coerce')
    if 'id' not in df.columns:
        # generate id with hash of relevant fields
        df['id'] = df.apply(lambda r: hashlib.sha1((''.join(map(str, [r.get('user.id'), r.get('timestamp')] ))).encode()).hexdigest(), axis=1)
    return df

Step 6: write to CSV or load into the database in chunks

Write to CSV in chunks or use a connector to your database. When writing to CSV use header on the first write and append without header for subsequent chunks.

output_csv = 'out.csv'
first = True
for chunk in read_jsonl_in_chunks('data.jsonl'):
    objs = [o for o in chunk if o is not None]
    df = process_chunk(objs)
    df = ensure_types(df)
    if first:
        df.to_csv(output_csv, index=False, mode='w', encoding='utf-8')
        first = False
    else:
        df.to_csv(output_csv, index=False, mode='a', header=False, encoding='utf-8')

Verify the result

Open the resulting CSV or query the database table and verify: coherent row count, expected columns, converted timestamps and absence of known errors. Also check a sample of records with exploded events to confirm alignment.

Conclusion

With these steps you have a practical ETL flow to parse JSONL files, normalize structures and load for analysis. Next steps: add tests, schema validation with fastjsonschema and support for compression (gzip). Tip: create small samples and validate before processing large files to avoid surprises.