By EasyCatch Growth Desk — weekly technical deep-dive for the Shopify community. Written 2026-10-26.
TL;DR
Supplier catalogs are not built around Shopify’s option model. A clothing SKU on a domestic wholesale marketplace routinely carries four attributes — colour, size, fabric weight, and print — while a product CSV exposes exactly three option slots. Two threads on this forum keep circling the same gap: a clothing-store thread asking how merchants actually check supplier files before importing, where a responder zeroes in on the real pain (“especially when supplier files have a lot of variants … whether the comparison can also show exactly which row or field changed”), and a much older import failure reported as Option value provided for unknown options, where the accepted answer spells out that there are only “three option spaces: Option 1 Name/Value, Option 2 Name/Value, Option 3 Name/Value.”
That second answer is the whole article in one sentence. When a supplier has more attributes than you have slots, you do not get an error — you get a decision. You collapse two attributes into a single option value (“Black / Heavyweight”), and from that moment the option value string becomes the identity of the variant. Everything downstream keys off that string: the per-row Variant Image binding, the swatch mapping, and every future re-import that tries to match existing rows. This is a data-model problem with a naming surface, not a technique problem — which is why the same store can import “successfully” for months and still ship the wrong photo to customers.
1. Three tables, one column that decides whether an image actually lands
A Shopify product import is not one table. It is three overlapping ones, serialised into a single flat file:
- products — one row per product, keyed by
Handle. This row carries the product-level fields:Title,Body (HTML),Tags, and — critically — theOption1 Name,Option2 Name,Option3 Namedeclarations. - variants — one row per purchasable SKU, repeating the same
Handleand supplying the values (Option1 Value,Option2 Value,Option3 Value), plusVariant SKU,Variant Price,Variant Grams. - images — separate rows that carry
Image SrcandImage Position, usually with the variant-specific fields left empty, plus a per-variant binding column:Variant Image.
The binding rule is the part people get wrong, and it is unforgiving in a specific way: Variant Image is matched against the image URL as a string. If the value on a variant row does not byte-for-byte match an Image Src declared for that same product, the variant simply has no image. No error, no warning, no row rejection — the product imports, the variant exists, the front end falls back to the featured image, and the customer sees a colour they did not order. Note also that the option names live on the product row while the option values live on variant rows; mixing a name into a value column is exactly how you produce Option value provided for unknown options rather than a useful message.
This is why the clothing-thread responder’s request — “show exactly which row or field changed” — is the correct instinct. Row-level diffing is the only diagnostic that catches a broken Variant Image string, because the import report will never mention it.
2. The three-slot ceiling and the error that is not the one you expect
Shopify’s variant model exposes three option dimensions. The CSV mirrors that with Option1/2/3 Name and Option1/2/3 Value, and the forum answer above states it plainly. There is no Option4 Name column to add, and adding one does not extend the model — the column is ignored, and any value you expected it to carry has nowhere to go.
Two failure signatures follow, and they look nothing alike:
- Loud failure. A variant row supplies a value whose option name was never declared on the product row — or the option name is missing entirely while values are present. Shopify rejects the row with
Option value provided for unknown options. This is the good outcome: the import stops and tells you something. - Silent failure. You keep three option columns but pack a fourth attribute inside one of the values, e.g.
Option1 Value = Black / Heavyweight. Import succeeds. The product page now shows a single dropdown whose entries are compound strings. Every downstream consumer that assumedOption1 Valueis a clean colour token — swatch mapping, filter apps, feed syndication, your own re-import matcher — silently loses that assumption.
The asymmetry matters: the loud error gets fixed in ten minutes, the silent one survives for quarters. If your supplier file has a fourth attribute, assume you are in case 2 until proven otherwise.
3. Three ways to collapse a fourth attribute, and what each one costs
Once you accept the ceiling, there are only three strategies. Each has a concrete, predictable breakage:
Strategy A — concatenate into one option value. Black / Heavyweight, Black / Lightweight, White / Heavyweight. Clean to generate, brutal downstream: swatch logic matches exact strings, so colour swatches stop resolving; sorting is alphabetical on the compound string, so all Black variants are not even adjacent unless your padding happens to agree; and any future supplier rename (Heavy Weight vs Heavyweight) creates a new variant instead of updating the old one. This is the most common choice and the most common source of duplicate variants on re-import.
Strategy B — promote one attribute to a second product. Each fabric weight becomes its own product with its own Handle, carrying only colour and size as options. The data model stays clean and swatches keep working. The cost moves to the catalog: N products instead of one, split reviews, split inventory reporting, and a collections/filter strategy you have to maintain. It is legitimate — but it is a merchandising decision, and it should be made deliberately rather than discovered after import.
Strategy C — drop the attribute from the variant model and move it to metafields or the description. The attribute still exists for the customer, just not as a purchasable dimension. Correct when the fourth attribute does not change the price or the SKU — if it does change either, you have silently made two SKUs into one and your inventory count is now fiction.
The decision is not aesthetic. Ask one question: does the fourth attribute change price, SKU, or stock? If yes, you cannot take Strategy C. If no, Strategy A is usually fine — provided you own the string-normalisation step and never let a supplier’s spelling variant create a second variant.
4. Cartesian expansion is where the images quietly die
Whichever strategy you pick, the row count is a product, not a sum. Five colours × eight sizes is 40 rows; add a third dimension of three collapsed fabric values and you are at 120 rows for a single product. Every one of those rows is an independent variant, and every one of them needs its own Variant Image value — the same image URL repeated, once per row, for each row that shares that colour.
The classic failure is writing the image only on the first row of each colour group. Rows are not grouped; the file is flat. Row 1 gets the image, rows 2-8 of the same colour get an empty Variant Image, and the storefront shows eight sizes of the correct colour with no photo. Then someone “fixes” it by setting the product’s featured image, which masks the problem on the default variant and hides it on all the others.
A second, subtler failure comes from the URL itself. Supplier CDN URLs frequently carry query strings — resize parameters, region tags, signed expiries. Image Src and Variant Image must be the same string, so the moment you normalise one and not the other (strip the query string in one column, keep it in the other), the binding breaks even though both URLs point at the same picture. Consistency between the two columns is worth more than prettiness in either.
5. What the transcode actually does to a per-row image
Supplier images are usually WebP, often wide-gamut, sometimes EXIF-rotated. If you convert them before upload — which you should, because a file whose bytes do not match its extension is the single most reliable way to get an image dropped at the CDN edge — the conversion itself introduces three specific losses that show up as colour complaints, not as errors:
- Alpha becomes black. JPEG has no alpha channel. Drawing a transparent WebP onto an uninitialised canvas composites it against black; the correct order is fill the canvas white first, then draw. Skipped, every transparent-background product shot gets a black rectangle behind it.
- Colour profile is clipped. Canvas defaults to sRGB. A Display P3 or Adobe RGB source is clipped into sRGB on
drawImage, andtoDataURL('image/jpeg')writes no ICC profile — so the browser that rendered it “looks fine” while the phone that receives it shows a duller, shifted red. This is the mechanism behind “the photo looked right on my laptop.” - EXIF orientation is discarded. Canvas ignores the orientation tag. A portrait supplier photo with a rotation flag comes out sideways, and it comes out sideways consistently, which makes it look like a supplier error rather than a pipeline step.
None of these produce a failed row. They produce a batch that imports cleanly and generates returns — which is why the fix belongs upstream of the CSV, in the conversion step, not in a post-import cleanup pass.
The white-fill ordering is the one part of this that is purely a code decision, and it is worth getting right once: a Local Canvas Transpiler that fills the canvas before calling drawImage eliminates the black-backdrop class of defect entirely, because the composite has an opaque destination from the first pixel. The colour-profile and EXIF losses are physics — no converter recovers what the format discards — but they can at least be known rather than discovered in a refund request. Running that conversion 100% Local-First also means the supplier photos never leave the machine they were downloaded to, which matters when the file set is the whole of next season’s catalog.
Conclusion
The through-line across all five sections is the same: the CSV is a serialisation of a model with three option dimensions, one string-keyed image binding, and no per-row grouping. Every failure above is that model asserting itself against a supplier file that never agreed to it. Before importing a batch, check the following — in this order, because each step invalidates the next if it fails:
- Count attributes, not columns. If the supplier spec has more than three purchasable dimensions, choose Strategy A, B, or C before generating the file — and write down the choice.
- Normalise option values once, centrally. One canonical token per colour, per size. Supplier spelling variants must never reach the CSV as distinct values.
- Assert row count = cartesian product. 5 colours × 8 sizes must equal 40 rows; if it does not, something collapsed silently.
- Assert
Variant Imageis non-empty on every row, and that each value exactly matches anImage Srcstring for the sameHandle— query strings included. A round-trip through a Matrixify CSV export will surface a broken binding immediately, because the export reads back what the store actually stored rather than what you sent. - Pre-encode images to JPEG or PNG before upload, filling white behind transparency and accepting that the ICC profile will not survive.
- Re-import diff on
Handle+ option values, never onTitle. Title changes are cosmetic; option value changes create new variants.
About EasyCatch
EasyCatch is a 100% Local-First copilot for 1688-to-Shopify sourcing. Its Local Canvas Transpiler converts supplier WebP images to JPG entirely inside your browser — filling behind transparency before the draw, so per-row images survive the transcode instead of arriving with black backdrops — and its CSV engine expands colour-by-size combinations into Matrixify-compliant ZIPs with the image URL repeated on every variant row, not just the first. Option values are normalised once, at extraction, so a supplier’s spelling variant cannot become a second variant. No supplier link, price, or product data ever leaves your machine: Zero-Server Privacy by design.
Written by Chen Yu
EasyCatch Growth Desk. Weekly technical deep-dives for the Shopify community.