Skip to Content

Product mapping

A product is one StagingProduct shape per source row. If the product has variants, nest them in input_variants as more StagingProduct shapes — variants reuse the same staging fields as the parent.

Quick reference — minimum loadable product

{ "original_id": entity_id, "title": name, "location_id": $$.config.default_location_gid, "price": $string(price), "sku": sku, "inventoryQuantity": qty }

That’s the smallest viable mapping. location_id is the only non-obvious required field — Graftport needs it to know where to assign inventory. Pull it from your migration’s location config.

status defaults to DRAFT. Loaded products start as drafts unless you override. Set "status": "ACTIVE" once you’re ready for them to be visible on the storefront.

Identity & display

The fields customers see and the ones that route URLs.

titlestringrequired

Product name customers see. Used to construct the URL handle if you don’t set one explicitly.

"title": name
handlestring

URL slug. Letters, hyphens, numbers only. Auto-derived from title if omitted.

"handle": $lowercase(url_key)
vendorstring

Brand or supplier name. Indexed for storefront filters.

"vendor": manufacturer
statusenumdefault DRAFT

One of ACTIVE, ARCHIVED, DRAFT. Defaults to DRAFT so migrated products don’t accidentally go live mid-migration.

"status": status = "enabled" ? "ACTIVE" : "DRAFT"
descriptionHtmlstring

Product description with HTML markup (<strong>, <em>, lists, etc.). Plain text works too.

"descriptionHtml": description
product_tagsstring[]default []

Searchable tags. The staging model deduplicates and sorts before emitting to Shopify (so ["red", "RED", "red"] becomes ["RED", "red"]).

"product_tags": $append(categories, ["migrated"])
positioninteger

Sort position within collections. Lower = earlier.

Pricing

All money values are strings — Shopify's API requires it.

pricestring

The price customers pay. Always a string, even if your source has it as a number — wrap with $string().

"price": $string(price)
compareAtPricestring

The “compare-at” / strikethrough price for showing a discount. String, like price.

"compareAtPrice": rrp > 0 ? $string(rrp) : null
coststringdefault 0

Unit cost (what the merchant pays for the product). Used for margin reporting in Shopify, not shown to customers.

"cost": $string(cost_per_unit)
taxablebooleandefault true

Whether the product is taxable.

taxCodestring

Tax classification code. Use only if the merchant has a custom tax service that requires specific codes.

price_listarray

Optional list of price entries for multi-currency or per-market pricing. Each entry shape depends on the merchant’s market setup.

Inventory

One product, one inventory quantity, at one location.

location_idstringrequired

The Shopify location GID where this product’s inventory is held. Get it from your migration’s location config (run Resync locations on the migration if it’s not populated yet).

"location_id": $$.config.default_location_gid

For multi-location merchants, pick the location based on the source row.

inventoryQuantityintegerdefault 0

Available quantity at location_id.

"inventoryQuantity": qty_in_stock
inventoryPolicyenumdefault CONTINUE

What happens when stock hits zero:

  • DENY — block customers from ordering.
  • CONTINUE — allow ordering anyway (oversell).
"inventoryPolicy": allow_backorder ? "CONTINUE" : "DENY"
stockLowintegerdefault 0

Low-stock threshold for the merchant’s internal alerts. Doesn’t affect Shopify’s own low-stock notifications.

minAmountintegerdefault 1

Minimum purchase quantity. Stored as a metafield, not a hard constraint — surface this in the theme if the merchant cares.

Identifiers

The codes that link the product back to inventory and barcode systems.

skustring

Stock keeping unit. Should be unique per shop. On a product without variants, this is the variant SKU.

"sku": sku
barcodestring

UPC, EAN, GTIN, ISBN — whichever the merchant uses.

weightnumberdefault 0

Product weight in the merchant’s chosen unit (set on the shop, not per product).

"weight": weight_kg
supplierNumberstring

Supplier’s internal product number. Stored as a metafield.

giftCardboolean

Set true if this product is a gift card SKU (rather than a gift card balance — for that, use the gift card resource).

Variants & options

Two ways to express variants: nested products or numbered option columns. Use one consistently.

input_variantsStagingProduct[]default []

Array of nested product shapes — each describes one variant. The nested shapes use the same fields as the parent (price, sku, inventoryQuantity, optionValues, etc.).

If input_variants is empty, Graftport auto-creates one variant from the parent product itself with the synthetic option Title = Default Title.

"input_variants": children.{ "original_id": child_id, "sku": sku, "price": $string(price), "inventoryQuantity": qty, "location_id": $$.config.default_location_gid, "options": [ { "optionName": "Size", "name": size }, { "optionName": "Color", "name": color } ] }
optionsobject[]

Variant-level options as an explicit list of { optionName, name } pairs. Used inside input_variants to declare which option values that variant has.

"options": [ { "optionName": "Size", "name": size } ]
option_name_1, option_value_1, …_2, …_3string

Alternative way to declare options on a variant: up to three pairs of option_name_N + option_value_N. Equivalent to options but flatter — sometimes easier when the source already has columns like color, size, length.

{ "option_name_1": "Color", "option_value_1": color, "option_name_2": "Size", "option_value_2": size }

Pick one form per variant — don’t mix options and option_name_N. Mixing is allowed (they’re concatenated) but harder to reason about.

product_idstring

The parent product’s external ID. Set on variants (entries inside input_variants) so Graftport knows which product they belong to. The parent’s original_id is the natural value.

variant_metafieldsobject

Variant-scoped metafields. Same shape as dict_metafields, but only set on variants — the parent’s dict_metafields describe the product itself.

"variant_metafields": { "warehouse_bin": bin_location }

Images & files

One product, one variant, two different file shapes.

filesFileSetInput[]default []

Array of images attached to the product (not a specific variant). Each entry needs at minimum originalSource (the URL Shopify will download from).

"files": gallery.{ "originalSource": "https://cdn.example.com/" & path, "alt": label, "contentType": "IMAGE" }

contentType is one of IMAGE, VIDEO, MODEL_3D. Defaults to IMAGE.

Graftport auto-matches files to variants by checking if the option value (e.g. "red") appears in the file URL — variants that match get the file assigned automatically. So if your file URLs follow product-red.jpg, product-blue.jpg, etc., the right image lands on the right variant.

fileFileSetInput

A single file attached to a variant. Set this inside an input_variants entry to pin one specific image to one variant.

"input_variants": children.{ "sku": sku, "file": { "originalSource": image_url, "contentType": "IMAGE" } }

If you set the parent’s files and let Graftport’s filename matching kick in, you usually don’t need to set per-variant file manually.

SEO

Flat fields that compose into the Shopify SEO object.

meta_titlestring

The <title> tag on the product page. Defaults to the product title if omitted.

"meta_title": seo.title
meta_descriptionstring

The meta description shown in search results.

"meta_description": seo.description

Collections

Manual collection membership. Smart collections live on the collection resource.

collectionsstring[]

Array of source-platform collection IDs (or Shopify GIDs) this product belongs to in manual collections. Graftport resolves source IDs to Shopify GIDs at load time using the collection migration’s results.

"collections": category_ids

Patterns

Single-variant product

A simple product with no variant axis — just one SKU, one price.

{ "original_id": entity_id, "title": name, "handle": $lowercase(url_key), "vendor": brand, "status": status = "1" ? "ACTIVE" : "DRAFT", "descriptionHtml": description, "sku": sku, "price": $string(price), "compareAtPrice": rrp > price ? $string(rrp) : null, "weight": weight, "barcode": ean, "inventoryQuantity": qty, "location_id": $$.config.default_location_gid, "files": images.{ "originalSource": url, "alt": alt }, "product_tags": tags, "meta_title": seo_title, "meta_description": seo_description }

Configurable product → multi-variant Shopify product

Source has a parent + N children with size + color. Flatten:

{ "original_id": parent_id, "title": name, "handle": $lowercase(url_key), "status": "ACTIVE", "descriptionHtml": description, "vendor": brand, "files": images.{ "originalSource": url, "alt": label }, "input_variants": children.{ "original_id": child_id, "product_id": $$.parent_id, "sku": sku, "price": $string(price), "inventoryQuantity": qty, "location_id": $$.config.default_location_gid, "options": [ { "optionName": "Color", "name": color }, { "optionName": "Size", "name": size } ] } }

location_id is required on the parent and on each variant.

Custom attributes → metafields

Pour every source-side custom attribute into dict_metafields and let auto-typing handle it:

{ "original_id": entity_id, "title": name, "location_id": $$.config.default_location_gid, "dict_metafields": $reduce( custom_attributes, function($acc, $attr) { $merge([$acc, { $attr.code: $attr.value }]) }, {} ), "metafield_prefix": "magento" }

Forcing a metafield type

When auto-detection picks the wrong type — e.g. you want color instead of single_line_text_field:

"dict_metafields": { "primary_color": { "value": color_hex, "type": "color" }, "launch_date": { "value": released_at, "type": "date" } }

Skipping draft / test products

{ "original_id": entity_id, "exclude": status = "draft" or sku ~> /^TEST/, }

Gotchas

Money is always strings. "price": 19.99 fails; "price": "19.99" works. Use $string(...).

  • Status defaults to DRAFT. Migrated products are invisible on the storefront until you set status: "ACTIVE" (or change them manually after the migration).
  • location_id is required. Run Resync locations on the migration before mapping inventory; without it, the variant load fails.
  • Three options max. Shopify rejects products with more than 3 options. The staging model raises OPTIONS_OVER_LIMIT early so you see the error in transform, not load.
  • Variant option values must match the parent’s option set. If a variant has Color = Red but no other variant introduces Red to the Color option, the staging model raises OPTION_DOES_NOT_EXIST. Check for typos and stray whitespace.
  • No two variants can share the exact same option combination. DUPLICATE_VARIANT fires if two variants both have e.g. Color=Red, Size=M.
  • Image-to-variant matching is fuzzy: the staging layer matches by checking if the option value appears (case-insensitive) in the file URL. If your file URLs don’t include option values, set file explicitly on each variant.
  • original_id becomes a metafield, so future runs can match. Don’t drop it.
Last updated on