Conversion

JSON to product feed

Headless storefronts, PIM exports and supplier APIs hand you products as JSON: one object per product, with variants, images and options nested inside. Shopping channels want the opposite, one flat row per sellable variant with their own column names. Google Merchant Center, Meta and Pinterest do not take a JSON file, and neither does Textfinity. So the conversion has two steps: flatten the JSON into a CSV with one row per variant, then map that CSV to each channel. This page gives a short jq command and a short Python script for the first step, and shows how Textfinity handles the second on a schedule.

We connect your store, build the feed and keep it in sync. Updated 2026-10-27.

At a glance

  • No major shopping channel takes a nested JSON catalog. Each wants flat rows, one per sellable variant, in its own attribute names.
  • Textfinity does not read JSON either. If you paste a JSON URL it tells you to export CSV instead. It reads CSV or TSV by URL (HTTPS or SFTP), Google Sheets, Google Shopping-format XML and Shopify.
  • Flattening is a short job. Use the platform's own CSV export if it has one; otherwise a short jq filter or a short Python script writes one row per variant.
  • Run the flatten on a schedule and publish the CSV at a fixed URL. Textfinity re-reads it on the shop's sync and rebuilds a CSV or TSV feed per channel.

Why JSON has to be flattened first

A JSON product export is a tree. In the example below, one product object holds a variants array, an images array and option values, so two products carry three sellable items. A product feed is a table: Google's product data specification wants one row per variant, linked by item_group_id, with id, price, availability, color and size on every row. Meta's catalog and Pinterest's catalog work the same way. Nothing in that chain walks a nested tree for you. Merchant Center's file formats are delimited text and RSS or Atom XML, plus Google Sheets and the Merchant API. Meta and Pinterest take CSV, TSV and XML.

Textfinity sits after the flatten. Its sources are Shopify (native, with variants), a CSV or other delimited file reachable by URL or SFTP, Google Shopping-format XML, and Google Sheets. A JSON URL is detected only so the app can ask you for a CSV. That keeps the flattening rule (which object is a row, which array becomes a column) in a script you own and can read, and the channel rules (column names, currency, availability words) in Textfinity, where they apply to every feed.

The source: a typical nested product JSON

This is the shape most headless platforms and product APIs return by default: product-level keys (id, title, vendor, url), an images array of objects, and a variants array with sku, price, inventory and option values. Prices arrive as strings with no currency, and inventory is a count rather than a word. Both recipes below turn exactly this file into the CSV in the download.

products.json (source)

[
  {"id": 8821, "title": "Ribbed Crop Tank", "vendor": "Loom",
   "url": "https://store.example/p/ribbed-tank",
   "images": [{"src": "https://cdn.example/tank-1.jpg"},
              {"src": "https://cdn.example/tank-2.jpg"}],
   "variants": [
     {"sku": "TANK-SAGE-S", "price": "24.00", "inventory": 12,
      "option1": "Sage", "option2": "S"},
     {"sku": "TANK-SAGE-M", "price": "24.00", "inventory": 0,
      "option1": "Sage", "option2": "M"}
   ]},
  {"id": 9912, "title": "Linen Lampshade", "vendor": "Hearth",
   "url": "https://store.example/p/linen-lampshade",
   "images": [{"src": "https://cdn.example/lamp-1.jpg"}],
   "variants": [
     {"sku": "LS-LINEN-01", "price": "59.00", "inventory": 4,
      "barcode": "00195866188174"}
   ]}
]

Flatten it with jq

jq is a small command-line JSON processor available in most package managers and CI images. The filter prints a header, then, for every variant of every product, one array combining the parent's keys with the variant's keys, written out as CSV. The first image becomes image_link and the remaining images are joined with commas into more_images. jq's @csv quotes every text field; the values are the same as the Python output below.

flatten with jq

jq -r '
["product_id","title","vendor","url","image_link","more_images",
 "sku","price","inventory","option1","option2","barcode"],
(.[] | . as $p | .variants[] |
  [$p.id, $p.title, $p.vendor, $p.url, $p.images[0].src,
   ([$p.images[1:][].src] | join(",")),
   .sku, .price, .inventory, .option1, .option2, .barcode])
| @csv' products.json > products.csv

Or flatten it with Python

The same logic in Python's standard library, for when the flatten runs inside an existing script or needs to call a paginated API first. Missing keys (the lampshade has no options, the tank has no barcode) become empty cells, not errors. For large catalogs, write rows as you read pages instead of loading the whole API response at once; the loop body stays the same.

flatten.py

import csv, json

products = json.load(open("products.json"))
cols = ["product_id", "title", "vendor", "url", "image_link", "more_images",
        "sku", "price", "inventory", "option1", "option2", "barcode"]
with open("products.csv", "w", newline="") as f:
    out = csv.writer(f, lineterminator="\n")
    out.writerow(cols)
    for p in products:
        imgs = [i["src"] for i in p.get("images", [])]
        for v in p["variants"]:
            out.writerow([p["id"], p["title"], p["vendor"], p["url"],
                          imgs[0] if imgs else "", ",".join(imgs[1:]),
                          v["sku"], v["price"], v.get("inventory", ""),
                          v.get("option1", ""), v.get("option2", ""),
                          v.get("barcode", "")])

From the flat CSV to channel feeds in Textfinity

Publish products.csv at a stable address: a web server or storage bucket over HTTPS, an SFTP server, or a Google Sheet the script writes to. We add that address as the source in Textfinity. It detects the delimiter, lists the columns, counts the products and runs feed hygiene, which reports fill rate per column, over-long values, mixed languages, inconsistent spellings and malformed image URLs. From there each channel gets its own feed from the same source: the Google Shopping, Meta Ads, Pinterest Ads, TikTok Ads, Microsoft Ads, Snapchat Ads and Amazon Ads templates pre-name the columns, and a custom feed covers channels without a template, such as Idealo or Awin.

Every feed is a CSV or TSV file on a stable URL, rebuilt on the shop's sync schedule and within about five minutes of a mapping edit (median rebuild 10.9 seconds on our platform). Merchant Center fetches that CSV in production on a scheduled fetch, even though Google's help page lists .txt, .tsv and .xml by extension. Commerce Manager and the other channels fetch their feed URL the same way; we register each one with you.

Flat CSV columnGoogle ShoppingMeta catalogPinterestTransform in Textfinity
skuidididrename
product_iditem_group_iditem_group_iditem_group_idrename
title + option1 + option2titletitletitleadvanced: if [option1] is empty then [title] else [title] + " – " + [option1] + " " + [option2]
urllinklinklinkrename
image_linkimage_linkimage_linkimage_linkrename
more_imagesadditional_image_linkadditional_image_linkadditional_image_linkrename (comma-separated list)
pricepricepricepriceadd suffix " USD"
inventoryavailabilityavailabilityavailabilityadvanced: if [inventory] equals "0" then "out_of_stock" else "in_stock" (Meta and Pinterest: "out of stock" / "in stock")
vendorbrandbrandbrandrename
barcodegtingtingtinrename
option1 / option2color / sizecolor / sizecolor / sizerename
(absent)conditionconditionconditionadd static value "new"

Channel attribute names from Google's product data specification, Meta's catalog fields reference and Pinterest's catalog spec, checked 2026-10-27. The availability spelling differs: Google in_stock, Meta and Pinterest 'in stock'.

The Google Shopping feed that comes out

Three rows, one per variant, in Google's attribute names: the two tank sizes share item_group_id 8821, the sold-out size reads out_of_stock, the currency is attached to the price, and the lampshade keeps its plain title because it has no options.

Google Shopping feed (CSV)

id,item_group_id,title,link,image_link,additional_image_link,availability,price,brand,gtin,condition,color,size
TANK-SAGE-S,8821,Ribbed Crop Tank – Sage S,https://store.example/p/ribbed-tank,https://cdn.example/tank-1.jpg,https://cdn.example/tank-2.jpg,in_stock,24.00 USD,Loom,,new,Sage,S
TANK-SAGE-M,8821,Ribbed Crop Tank – Sage M,https://store.example/p/ribbed-tank,https://cdn.example/tank-1.jpg,https://cdn.example/tank-2.jpg,out_of_stock,24.00 USD,Loom,,new,Sage,M
LS-LINEN-01,9912,Linen Lampshade,https://store.example/p/linen-lampshade,https://cdn.example/lamp-1.jpg,,in_stock,59.00 USD,Hearth,00195866188174,new,,

Examples

Worked examples, by situation

Three JSON sources, three ways to get a flat CSV. Each shows the fragment that needed work and the route from it to a channel feed.

Headless storefront · Google Shopping · 3,000 variants · nested variants

Nightly jq job in CI, CSV in a bucket

Before

{"id": 8821, "title": "Ribbed Crop Tank", "variants": [{"sku": "TANK-SAGE-S", "option1": "Sage", "option2": "S", "price": "24.00"}]}

After

flat CSV: 8821,Ribbed Crop Tank,…,TANK-SAGE-S,24.00,12,Sage,S
Google feed: TANK-SAGE-S,8821,Ribbed Crop Tank – Sage S,…,in_stock,24.00 USD

How we set it up

The storefront's product API has no CSV export, so a scheduled CI job runs the jq filter above and uploads products.csv to a public bucket path. We add that URL as the source, build the Google Shopping feed and map id from sku, item_group_id from product_id, title by the advanced rule and price with add suffix " USD". If the storefront runs on Shopify, skip all of this and connect Shopify natively.

PIM export · Pinterest · 800 SKUs · image list and category path

The PIM's own CSV export, dropped on SFTP

Before

{"sku": "LAMP-T-02", "images": ["https://cdn.example/lamp-1.jpg", "https://cdn.example/lamp-2.jpg"], "category": {"path": "Home/Lighting/Table lamps"}}

After

image_link: https://cdn.example/lamp-1.jpg
additional_image_link: https://cdn.example/lamp-2.jpg
product_type: Home > Lighting > Table lamps

How we set it up

Most PIMs can export CSV directly, so use that instead of the JSON API: one row per SKU, images joined with commas in one quoted cell, scheduled nightly to an SFTP server Textfinity pulls from. We map image_link with remove from regexp ,.*$, which keeps the part before the first comma; additional_image_link with remove from regexp ^[^,]*,?, which keeps the rest, already comma-separated; product_type with replace from regexp / to ' > '; and google_product_category as a replace from google sheet on the path, conditioned on is in google sheet list.

Supplier API · Meta catalog · 12,000 SKUs · paginated, stock as a number, EUR

Python over the pages, availability words in Textfinity

Before

{"page": 3, "items": [{"sku": "HP-4421", "stock": 0, "backorder": true, "price": 149}]}

After

flat CSV: HP-4421,149,0,true
Meta feed: availability out of stock · price 149.00 EUR

How we set it up

A cron job runs the Python script with a loop over the API's pages, writing each item as a row, and uploads the CSV to SFTP. In Textfinity we map availability with the advanced expression if [stock] equals "0" then "out of stock" else "in stock" and price with [price] + ".00 EUR" for whole-euro prices, and add an exclude filter "stock is less or equal to 0" when sold-out items should leave the catalog.

Copy this plan

Plan: JSON source to channel feeds

An afternoon, most of it on the flatten. After that, we set up the channel work in Textfinity and it repeats on its own.

  1. 01

    Look for a CSV export before writing code

    Most platforms and PIMs that serve JSON can also export CSV, and a Shopify storefront connects natively. A built-in export is maintained by the vendor; a script is maintained by you.

  2. 02

    Flatten to one row per variant

    Run the jq filter or the Python script against the JSON. Keep product-level keys on every variant row and give each variant its own sku. Check the row count against the number of variants.

  3. 03

    Publish the CSV on a schedule

    Schedule the flatten (cron, CI or the PIM's export scheduler) and write the CSV to a fixed HTTPS path, an SFTP server or a Google Sheet.

    What we set up: You send us the address; we add the CSV URL (HTTPS or SFTP) or Google Sheet as the source in Textfinity.
  4. 04

    Create a feed per channel

    Pick the channel template: Google Shopping, Meta Ads, Pinterest Ads, TikTok Ads, Microsoft Ads, Snapchat Ads, Amazon Ads, eBay, Kelkoo, Prisjakt or PriceRunner. Use a custom feed with the channel's columns for anything else.

    What we set up: We build each feed from its channel template, or as a custom feed.
  5. 05

    Map identifiers, numbers and words

    id from sku, item_group_id from the product id, title by combine or an advanced rule, currency suffix on price, an advanced rule for availability, a static value for condition.

    What we set up: We map id as a rename, price with add suffix and availability with an advanced rule.
  6. 06

    Filter, check hygiene and publish

    Exclude rows without an image or a price, read feed hygiene for empty required columns, then give each channel the feed URL to fetch on a schedule.

    What we set up: We add the filter rules, read feed hygiene and register each feed URL with you in the channel.

Customization tips

Make it yours

Decide the row level before you flatten

Variant rows give channels exact sizes, prices and stock but multiply the SKU count (and the catalog size you are quoted on). Product rows suit single-variant catalogs. Pick once and keep ids stable.

Keep ids from the source, not from position

An array index shifts when a product is deleted; a SKU does not. Use sku or a stable product id so channels keep performance history across rebuilds.

Flatten in the script, rename in Textfinity

Let the script decide only rows and columns, using the source's own key names. Channel spelling, currency and availability words belong in mapping rules, where one change updates every feed.

Paginated APIs need one finished file

Textfinity reads one URL per shop. Collect every page into the CSV before you publish it, and write to a temporary name then rename, so a fetch never sees a half-written file.

FAQ

Questions about json to product feed

Can Google Merchant Center read a JSON product feed?

No. Merchant Center takes delimited text and RSS or Atom XML files (Google's help page lists .txt, .tsv and .xml), Google Sheets and Merchant API submissions, not a JSON file. Flatten the JSON to one row per variant first. Merchant Center does fetch Textfinity's comma-delimited CSV in production on a scheduled fetch.

Does Textfinity read JSON?

No. A JSON URL is detected only so the app can ask for a CSV instead. Flatten the JSON with the platform's CSV export, the jq filter or the Python script on this page, publish the CSV by URL, SFTP or Google Sheet, and we map it to each channel in Textfinity, which rebuilds on the shop's schedule.

How do I convert nested JSON to CSV with one row per variant?

Loop over products, then over each product's variants, and write one row per variant that repeats the product-level keys (id, title, brand, url, first image). Join the remaining images into one comma-separated cell. The jq filter and the Python script above do exactly this; missing keys become empty cells.

Can one JSON source produce feeds for several channels?

Yes, once it is a CSV. Each Textfinity feed has its own template, mapping and filters, and all of them rebuild from the same source. We set up each feed for you; the number of feeds is part of the per-catalog quote.

Can I set this up from code?

Textfinity has no public REST API. Clients can connect an AI assistant such as Claude to Textfinity over MCP; it can list shops and feeds, create a feed, add mapping and filter rules, run feed hygiene and read a feed's output. The flatten itself stays in your script.

Set up for you

Flatten the JSON once; we set up the feeds.

Publish the flat CSV by URL, SFTP or Google Sheet and send us the address. We pick the channel templates and map the columns in Textfinity, and the software rebuilds every channel feed on a schedule.

Have us map my CSVOr email support@textfinity.io

Tell us your store and channels. We reply within 24 hours.