Parquet vs JSONL vs CSV for OpenAI product feeds: what actually matters

OpenAI accepts parquet (preferred, with zstd), jsonl.gz, csv.gz and tsv.gz. The format decides how booleans, identifiers and nested fields survive export.

Published 10 min read

OpenAI accepts four feed formats: parquet, which the documentation prefers “ideally with zstd compression”, and gzipped JSON Lines, CSV and TSV. The content rules are identical across all four. What differs is how each format preserves types (booleans, decimals, identifiers with leading zeros) and nested values (variant_dict, additional_image_urls). Small catalogs are fine in jsonl.gz; large ones benefit from parquet.

What the documentation actually says

The file upload overview is short on the subject. It states the format preference (“Prefer parquet”, ideally with zstd compression, while “jsonl.gz, csv.gz, and tsv.gz are also supported”), requires UTF-8, recommends “Up to 500k items per shard” with “shard files under ~500MB”, asks for “a stable file name” overwritten on every update, wants “full snapshots on a predictable cadence (at least daily)”, says OpenAI “retains its most recently processed record for up to 14 days”, and confirms delivery is a push over SFTP with no completion marker needed.

There is no published benchmark of ingestion speed by format, and we have not measured one. So the honest comparison is not “which format is faster” but “which format makes it hardest to ship a malformed value”. That is where the four formats differ.

The four formats side by side

parquetjsonl.gzcsv.gztsv.gz
Documentation stancePreferred, ideally zstdSupportedSupportedSupported
Typed columnsYes (schema in the file)Per value (JSON types)No, everything is textNo, everything is text
BooleansNative booleanJSON true/falseLowercase strings true/falseLowercase strings true/false
Nested values (variant_dict, dimensions)Native struct or map, or a JSON string columnNative JSON objectJSON serialized inside a quoted cellJSON serialized inside a cell
Lists (additional_image_urls, target_countries)Native list, or delimited stringJSON arrayComma-separated string, commas in URLs as %2CComma-separated string
Readable with a text editorNoYesYesYes
Opens in a spreadsheetNoNoYes, with type-inference risksYes, same risks
Quoting rules to get rightNoneJSON escaping onlyCommas, quotes, newlinesTabs and newlines
Tooling needed to writeA parquet library (Arrow, pandas, DuckDB, Spark)Any languageAny language, a proper CSV writerAny language

The rest of this article explains each row of the table with the exact rule from the specification.

Parquet: typed columns, compression, and one trap

Parquet stores a schema with the data. A boolean column is a boolean, a string column is a string, and a list column is a list. That property is why the documentation prefers it: the format itself rules out the class of errors where a text column happens to contain TRUE or 1 where a boolean was expected.

Compression is built into the format per column, and zstd is the codec the overview points to. A catalog of repetitive values (the same brand, the same seller_name, the same availability on most rows) compresses well column by column.

The trap is type inference at write time. Most parquet writers infer a column’s type from the data. A gtin column whose values all look like numbers gets inferred as an integer, and the leading zero on 00012345678905 is gone before the file is written. The specification is direct about this: “Keep identifiers as strings to preserve leading zeros.” Declare the schema explicitly, with item_id, group_id, offer_id, gtin and mpn as strings, and price and sale_price as strings too, since they are money strings (79.99 USD), not decimals.

The second cost is legibility. You cannot open a parquet file in a text editor to check one row. You need a tool (DuckDB and pandas both read parquet in one line), and your team needs to be comfortable with that when a merchant asks why a product is missing.

JSONL: one JSON object per line

JSON Lines is the format the specification uses for its own examples. Each line is a complete JSON object; the file is gzipped. Types come from JSON: booleans are true and false, strings are quoted, objects and arrays are native. The nested fields of the specification map directly:

{"item_id":"MUG-350-BLUE","group_id":"MUG-350","listing_has_variations":true,"variant_dict":{"color":"Blue","capacity":"350 mL"},"title":"Blue ceramic mug, 350 mL","description":"Dishwasher-safe glazed ceramic mug with a handle.","url":"https://example.com/products/mug-blue","brand":"Northline","seller_name":"Northline Home","image_url":"https://example.com/images/mug-blue.jpg","additional_image_urls":["https://example.com/images/mug-blue-top.jpg","https://example.com/images/mug-blue-side.jpg"],"availability":"in_stock","price":"18.00 USD","gtin":"00012345678905","is_eligible_search":true,"target_countries":["US"]}

Two things to notice. gtin is a quoted string even though it is all digits, so the leading zeros survive. variant_dict and additional_image_urls are a real object and a real array, with no quoting gymnastics.

The JSONL trap is the opposite of parquet’s: nothing enforces consistency between lines. Line 1 can have "is_eligible_search": true and line 2 can have "is_eligible_search": "true". Both are accepted by a JSON parser; only the first is a JSON boolean. The specification allows the lowercase string form “in delimited files”, so keep JSON booleans in JSONL and let a schema check (or a validator) confirm every line has the same shape.

JSONL is also the easiest format to debug. zcat feed.jsonl.gz | grep MUG-350-BLUE shows you the row exactly as OpenAI receives it.

CSV and TSV: spreadsheet friendly, and the most ways to go wrong

Delimited files have no types. Everything is text, and the specification adapts its rules accordingly: booleans are “the lowercase strings true and false in delimited files”; “JSON objects in CSV or TSV cells must be serialized as JSON”; and for CSV, “quote a cell containing commas, quotes, or newlines, and double each embedded quote.”

Here is the header for the JSONL row above, and the row itself as CSV:

item_id,group_id,listing_has_variations,variant_dict,title,description,url,brand,seller_name,image_url,additional_image_urls,availability,price,gtin,is_eligible_search,target_countries
MUG-350-BLUE,MUG-350,true,"{""color"":""Blue"",""capacity"":""350 mL""}","Blue ceramic mug, 350 mL",Dishwasher-safe glazed ceramic mug with a handle.,https://example.com/products/mug-blue,Northline,Northline Home,https://example.com/images/mug-blue.jpg,"https://example.com/images/mug-blue-top.jpg,https://example.com/images/mug-blue-side.jpg",in_stock,18.00 USD,00012345678905,true,US

Every trap of the format is visible in that one line.

Commas inside values. The title Blue ceramic mug, 350 mL contains a comma, so the cell is quoted. A hand-rolled exporter that joins fields with , and never quotes shifts every following column by one on that row: price lands in gtin, availability lands in price, and the row is malformed in three places.

JSON objects in cells. variant_dict is serialized as JSON, then the whole cell is quoted and every inner quote is doubled. Getting this right by hand is error prone; use a real CSV writer and pass it the JSON string.

Commas inside list values. additional_image_urls is one cell holding a comma-separated list, so the cell is quoted. If one of the image URLs itself contains a comma (some CDNs use commas in transformation parameters), the specification asks you to “Percent-encode commas as %2C in URL”, otherwise the list splits inside the URL.

Booleans as strings. listing_has_variations and is_eligible_search are the lowercase strings true. A spreadsheet that displays a checkbox or a formula result writes TRUE on export, which is not one of the two accepted spellings.

Leading zeros. 00012345678905 is text here, but open this CSV in a spreadsheet, save it, and the column becomes a number: 12345678905, 11 digits, invalid length, invalid GTIN. This is the most common way a valid CSV feed becomes an invalid one, and it happens without anyone editing the cell.

Newlines in descriptions. A description with paragraph breaks must be quoted, and a reader that splits on newlines before parsing quotes breaks the file. Most CSV libraries handle this; most quick scripts do not.

TSV shares all of these except comma handling: tabs are rarer in product data than commas, so fewer cells need attention, but a stray tab in a description still breaks the row, and JSON objects still need serializing. TSV is otherwise interchangeable with CSV for this purpose.

The advantage of delimited files is real, though: a merchandising team can open a 2,000-row CSV, sort by availability, and see the catalog. No other format offers that without tooling. The trade is that the same spreadsheet is the tool most likely to corrupt the file.

Rules that are the same in every format

The format changes how values are encoded, not what they must be. These apply to all four.

  • Sharding. “Up to 500k items per shard is recommended; target shard files under ~500MB”. Below those limits, a single file is fine. Above, split into several shards.
  • Stable file names. “Keep the same file name on every update and overwrite it with the latest snapshot instead of creating a new name each run.” Date-stamped file names accumulate instead of replacing.
  • Full snapshots, at least daily. Incremental deltas are not described. Every delivery is the whole catalog.
  • The 14-day tail. A product missing from a snapshot is retained “for up to 14 days”. To remove one faster, keep its row and set is_eligible_search=false.
  • UTF-8, absolute HTTP or HTTPS URLs, money as amount CURRENCY, no placeholders, identifiers as strings.
  • SFTP delivery, with no completion marker to send.
  • Start small. The overview asks you to “Start with a small sample (around 100 items)”. The sample is where you find out that your CSV writer did not quote, or that your parquet schema inferred an integer.

If a product is absent from ChatGPT and the format looks right, the row-level causes are listed in Why your products do not show up in ChatGPT shopping results.

An honest recommendation by catalog size

We have not benchmarked OpenAI’s ingestion across formats and the documentation publishes no such figures, so this recommendation is about correctness and operability, not speed.

Up to a few thousand products. Use jsonl.gz. The file is small whatever the format, so compression and typed columns buy you nothing measurable, while the ability to zcat and grep a row when a merchant asks a question is worth a lot. Booleans and nested fields are native, and the only discipline needed is to keep identifiers quoted.

Tens of thousands of products. Still jsonl.gz if you generate the file from code and validate it. Consider parquet if the file is produced by a data pipeline that already speaks Arrow or Spark, because then the typed schema comes for free and you avoid the boolean-as-string drift between lines.

Hundreds of thousands of products and beyond. Parquet with zstd, as the documentation prefers. At this size you are sharding anyway, columnar compression matters for transfer time over SFTP, and an explicit schema is the only reliable way to guarantee that half a million gtin values are all strings. Declare the schema; do not let the writer infer it.

CSV or TSV, at any size. Choose them only if the file must be edited or reviewed in a spreadsheet by people, and then treat the spreadsheet as read-only: export from the system of record, never re-save from the spreadsheet back into the feed. If you already maintain a Google Shopping CSV, the mapping differences are covered in Google Shopping feed vs OpenAI feed.

Common mistakes

  • Letting a spreadsheet touch the CSV between export and upload (leading zeros lost, TRUE written for booleans).
  • Writing parquet without an explicit schema, so gtin becomes an integer.
  • Mixing JSON booleans and string booleans between lines of the same JSONL file.
  • Joining CSV fields with a comma and no quoting.
  • Naming the file with a date so each run adds a file instead of replacing one.
  • Sending a delta of changed products instead of the full snapshot.
  • Skipping the 100-item sample and debugging the first delivery on the full catalog.

What Convrail does with formats

Convrail exports all four formats: parquet, jsonl.gz, csv.gz and tsv.gz. The choice is a setting; the validation is the same. Every row is checked against the specification before it is encoded, then written with the encoding the format requires: JSON booleans in JSONL and parquet, lowercase strings in CSV and TSV; identifiers always as strings; variant_dict serialized as JSON inside quoted cells in delimited files; commas quoted; UTF-8 throughout. Shards are split at 500,000 items or about 450 MB, below the recommended limits, named feed-organic-000.<ext>, feed-organic-001.<ext> and so on, and the same names are overwritten on every daily SFTP delivery at the hour you choose. Each run journals the items read, accepted and rejected and the per-row errors, so a format-level mistake never has to be found by opening the file. In our automated test, a 10,000-product catalog is validated, exported and delivered to SFTP in under a minute.

What to do next

Pick the format from a setting and let the validator handle the encoding rules on every row: see the product feed page.

Sources

Frequently asked questions

Which file formats does OpenAI accept for the product feed?

Four: parquet, which the documentation prefers ideally with zstd compression, plus gzipped JSON Lines (jsonl.gz), gzipped CSV (csv.gz) and gzipped TSV (tsv.gz). All must be UTF-8 and are pushed by SFTP.

Is parquet required for an OpenAI product feed?

No, it is preferred. A small catalog delivered as jsonl.gz is fully compliant; parquet becomes the sensible choice when the catalog is large enough that typed columns and compression matter.

How big can one feed file be?

The documentation recommends up to 500k items per shard and shard files under about 500 MB. Split larger catalogs into several files with stable names and overwrite them on every snapshot.

Can I send only the products that changed since yesterday?

No. The documentation asks for full snapshots on a predictable cadence, at least daily. A product missing from a snapshot is retained for up to 14 days and then expires.

Why do my leading zeros disappear in the feed?

Because the export tool typed the column as a number. GTINs and other identifiers must be strings in every format; in CSV the safest approach is a writer that never infers types.

Measure the ChatGPT channel this week

Free during early access while we onboard the first stores. Leave your email and we send the install link when your spot opens.

No newsletter. One email with the install link, nothing else.