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.
titlestringrequiredProduct name customers see. Used to construct the URL handle if you don’t set one explicitly.
"title": namehandlestringURL slug. Letters, hyphens, numbers only. Auto-derived from title
if omitted.
"handle": $lowercase(url_key)vendorstringBrand or supplier name. Indexed for storefront filters.
"vendor": manufacturerstatusenumdefault DRAFTOne of ACTIVE, ARCHIVED, DRAFT. Defaults to DRAFT so
migrated products don’t accidentally go live mid-migration.
"status": status = "enabled" ? "ACTIVE" : "DRAFT"descriptionHtmlstringProduct description with HTML markup (<strong>, <em>, lists,
etc.). Plain text works too.
"descriptionHtml": descriptionproduct_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"])positionintegerSort position within collections. Lower = earlier.
Pricing
All money values are strings — Shopify's API requires it.
pricestringThe price customers pay. Always a string, even if your source has it
as a number — wrap with $string().
"price": $string(price)compareAtPricestringThe “compare-at” / strikethrough price for showing a discount.
String, like price.
"compareAtPrice": rrp > 0 ? $string(rrp) : nullcoststringdefault 0Unit cost (what the merchant pays for the product). Used for margin reporting in Shopify, not shown to customers.
"cost": $string(cost_per_unit)taxablebooleandefault trueWhether the product is taxable.
taxCodestringTax classification code. Use only if the merchant has a custom tax service that requires specific codes.
price_listarrayOptional 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_idstringrequiredThe 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_gidFor multi-location merchants, pick the location based on the source row.
inventoryQuantityintegerdefault 0Available quantity at location_id.
"inventoryQuantity": qty_in_stockinventoryPolicyenumdefault CONTINUEWhat happens when stock hits zero:
DENY— block customers from ordering.CONTINUE— allow ordering anyway (oversell).
"inventoryPolicy": allow_backorder ? "CONTINUE" : "DENY"stockLowintegerdefault 0Low-stock threshold for the merchant’s internal alerts. Doesn’t affect Shopify’s own low-stock notifications.
minAmountintegerdefault 1Minimum 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.
skustringStock keeping unit. Should be unique per shop. On a product without variants, this is the variant SKU.
"sku": skubarcodestringUPC, EAN, GTIN, ISBN — whichever the merchant uses.
weightnumberdefault 0Product weight in the merchant’s chosen unit (set on the shop, not per product).
"weight": weight_kgsupplierNumberstringSupplier’s internal product number. Stored as a metafield.
giftCardbooleanSet 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, …_3stringAlternative 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_idstringThe 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_metafieldsobjectVariant-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.
fileFileSetInputA 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_titlestringThe <title> tag on the product page. Defaults to the product
title if omitted.
"meta_title": seo.titlemeta_descriptionstringThe meta description shown in search results.
"meta_description": seo.descriptionCollections
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_idsPatterns
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_idis 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_LIMITearly so you see the error in transform, not load. - Variant option values must match the parent’s option set. If a
variant has
Color = Redbut no other variant introducesRedto theColoroption, the staging model raisesOPTION_DOES_NOT_EXIST. Check for typos and stray whitespace. - No two variants can share the exact same option combination.
DUPLICATE_VARIANTfires 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
fileexplicitly on each variant. original_idbecomes a metafield, so future runs can match. Don’t drop it.