jsonlkit.com
JSONL (JSON Lines) utilities, in the browser
Say hi →

Loading JSONL into BigQuery

Destination guide · updated 28 July 2026 · overview · spec · format comparison · examples · best practices · troubleshooting · FAQ · glossary

BigQuery reads newline-delimited JSON natively — it is the only JSON shape it accepts for loading, which is why its docs say NEWLINE_DELIMITED_JSON everywhere. This covers the load command, when to let it autodetect a schema, how nested data maps, and the handful of errors that actually stop a load.

The load command

# From a local file, with schema autodetection
bq load \
  --source_format=NEWLINE_DELIMITED_JSON \
  --autodetect \
  mydataset.mytable \
  ./data.jsonl

# From Cloud Storage, with an explicit schema (recommended for production)
bq load \
  --source_format=NEWLINE_DELIMITED_JSON \
  mydataset.mytable \
  gs://my-bucket/data/*.jsonl.gz \
  ./schema.json

# Append rather than replace, and tolerate a few bad rows
bq load \
  --source_format=NEWLINE_DELIMITED_JSON \
  --autodetect \
  --max_bad_records=100 \
  mydataset.mytable \
  gs://my-bucket/data/*.jsonl

Gzipped files work directly — no decompression step. The wildcard loads every matching object, which is why sharding a large dataset with the splitter and loading the shards in parallel is faster than one enormous file.

Autodetect or explicit schema?

--autodetect samples up to 500 rows and infers types. Convenient for exploration, risky in a pipeline, for three specific reasons:

For anything recurring, write the schema down. The fastest route: infer a JSON Schema from the whole file with our schema inferrer — it reads every record, not a sample — then translate it. Check the result against field coverage, which shows exactly which fields are not present in every record and which hold more than one type.

A BigQuery schema file looks like this:

[
  {"name": "id",         "type": "INT64",     "mode": "REQUIRED"},
  {"name": "created_at", "type": "TIMESTAMP", "mode": "NULLABLE"},
  {"name": "tags",       "type": "STRING",    "mode": "REPEATED"},
  {"name": "user",       "type": "RECORD",    "mode": "NULLABLE",
   "fields": [
     {"name": "name",  "type": "STRING"},
     {"name": "email", "type": "STRING"}
   ]}
]

Nested and repeated fields

This is where BigQuery is genuinely good and unlike most warehouses: you do not have to flatten. A nested JSON object becomes a RECORD (a STRUCT in SQL), and an array becomes REPEATED. Query them with dots and UNNEST:

-- Nested object: just use a dot
SELECT id, user.email FROM mydataset.mytable;

-- Repeated field: cross join with UNNEST
SELECT id, tag
FROM mydataset.mytable, UNNEST(tags) AS tag;

-- Array of objects
SELECT id, item.sku, item.qty
FROM mydataset.mytable, UNNEST(items) AS item;

So resist flattening on the way in. Keeping the structure is cheaper to store, faster to query, and preserves information that flattening loses. Flattening is for targets that cannot do this — plain CSV, or a database without struct support.

The exception is genuinely unpredictable structure — user-supplied metadata with arbitrary keys. For that, load the field as a JSON column (BigQuery has a native JSON type) and query it with JSON_VALUE, rather than trying to describe every possible key in a schema.

Errors that actually stop a load

MessageCause and fix
Error while reading data … JSON parsing error A line is not valid JSON, or the file is not newline-delimited (typically a pretty-printed JSON array). Check with the validator; convert an array with JSON → JSONL.
no such field: X A record has a key the schema does not. Either add it, or pass --ignore_unknown_values to drop it. Note that dropping is silent data loss.
Could not convert value to integer Mixed types in one field — usually a number that is sometimes quoted, or an empty string where a number was expected. Field coverage finds these.
Invalid field name BigQuery column names allow only letters, digits and underscores, must not start with a digit, and are case-insensitive. Rename with the key case converter (snake_case is the right target) or find & replace.
Invalid timestamp A datetime string BigQuery cannot parse. It wants ISO 8601; 2026-07-28 10:00 without an offset is ambiguous and 28/07/2026 is not accepted at all.
Load succeeds, rows missing --max_bad_records is set and rows were skipped. The job's error stream lists them. Silence here is worse than failure — check the job details.

Preparing a file that will load first time

  1. Validate — every line must be parseable JSON.
  2. Fix the encoding — a BOM breaks line 1, and BigQuery expects UTF-8.
  3. Normalise key names to snake_case, so no column name is rejected.
  4. Check coverage and types — mixed-type fields are the main cause of a load failing part-way.
  5. Infer a schema from the whole file rather than relying on a 500-row sample.
  6. Make a 100-record sample and load that into a scratch table first. A ten-second test beats a forty-minute failure.
  7. Compress before uploading to Cloud Storage. BigQuery reads .gz directly.

Other warehouses, briefly

Frequently asked questions

Does BigQuery accept a JSON array file?

No. For loading it accepts only newline-delimited JSON, which is why --source_format=NEWLINE_DELIMITED_JSON exists and there is no array equivalent. Convert with JSON → JSONL first.

Should I gzip before uploading?

Yes for network transfer and storage cost — BigQuery reads .gz directly. One caveat: gzipped files cannot be read in parallel, so a single huge .gz loads more slowly than many uncompressed shards. For very large loads, shard first, then compress each shard.

What is the largest file I can load?

Limits change; consult the current quota page rather than a number in a blog post. The practical answer is to shard: many files of a few hundred megabytes load faster and fail more recoverably than one enormous one.

How do I handle records whose keys vary unpredictably?

Load that part as a JSON column and query with JSON_VALUE / JSON_QUERY. Trying to enumerate every possible key in a schema does not survive contact with real user-supplied data.

Why did my load succeed but with fewer rows than expected?

--max_bad_records allowed some rows to be skipped. Check the job's error details — the rows are listed there. Set it to 0 while developing so failures are loud.

Tools mentioned on this page

— S., [email protected]