Magento
Magento to Shopify migration: the serialized EAV trap
A 26-person Gent fashion e-tailer lost their 22 best-converting variant combinations during a Magento to Shopify Plus migration. Here is how, why, and the fix.

Day four of the cutover. The merchandising lead at a 26-person Gent fashion e-tailer is reconciling her best-seller report against the new Shopify Plus admin. Twenty-two variant combinations that drove last autumn's reorder budget show no signal at all. Same products, same SKUs, same images. The fit profile badge that used to flag low return, repeat buyer reads default on every size, every colour, every silhouette.
She pings the agency. The agency pings us. We open the database backup and find what we always find on Magento 2.4.4 stores that grew their own size logic: a serialized PHP array sitting in a column the migrator never knew to read.
Ten days of cutover stalled. One column. This is how it happens, how to recognise it, and how to keep it from happening on your store.
The serialized graveyard inside eav_attribute_option
Magento 2 stores attribute options (small, medium, large, plus colour swatches and any custom dropdown) in eav_attribute_option and eav_attribute_option_value. The first table is the option row. The second holds the human-readable label per store view. Standard Adobe Commerce, documented in the Adobe Commerce EAV reference.
What is not standard: a five-year-old in-house size-chart module that piggy-backed a per-option payload onto the same table, by way of a column the original developer added in a setup script in 2021. The column was called additional_data. It held a PHP-serialized array of signals the merchandising team had been quietly collecting for two seasons.
SELECT
eao.option_id,
ea.attribute_code,
eaov.value AS label,
eao.additional_data
FROM eav_attribute_option eao
JOIN eav_attribute_option_value eaov ON eao.option_id = eaov.option_id
JOIN eav_attribute ea ON eao.attribute_id = ea.attribute_id
WHERE ea.attribute_code IN ('size', 'fit_profile', 'silhouette')
AND eao.additional_data IS NOT NULL
ORDER BY ea.attribute_code, eao.sort_order;
The unserialized payload looked like this for every option row:
a:4:{
s:11:"return_rate";d:0.034;
s:8:"converts";d:0.082;
s:7:"reorder";b:1;
s:7:"profile";s:14:"low_return_fit";
}
Multiply by three attributes (size, fit profile, body silhouette) and seventeen colour-size grids and you get the 22 combinations that powered the autumn merchandising calendar. None of it surfaced in the admin UI. The team read it through a back-office report wired straight to that column, and a frontend renderer that swapped in icons based on the payload.
What the migrator silently did
The agency picked a respected off-the-shelf migrator. Names withheld, but it is one of the two everyone reading this is thinking of. The tool maps Magento 2 entities to Shopify resources, runs the cutover unattended, and prints a green tick. It is excellent at what it does. What it does not do is read columns nobody told it about.
The migrator's attribute-option pass reads eav_attribute_option_value.value (the label), maps it to a Shopify option value, and moves on. Anything in additional_data is invisible. There is no warning. There is no log line. The tick is green because the visible data round-tripped fine.
On the Shopify side, every variant inherited its size from the standard option label. The fit-profile attribute, which had been the merchandising team's quiet weapon, landed as a single Shopify metafield with the string value default across every product. The migrator wrote it because the attribute existed. It wrote default because the actual labels in eav_attribute_option_value for that attribute were literally the string "default" (the team had hidden the labels behind the frontend renderer and never bothered updating them).
If your Magento store has a custom column on an EAV table, no commercial migrator will read it. The only way to know is to SHOW CREATE TABLE every EAV table on the source and diff against a stock 2.4.x install.
How we found the missing 22 combinations
Day four of cutover, the agency had already pointed DNS. Rolling back was a five-figure conversation with the new Shopify Plus contract and a marketing campaign already running. So we worked from the Magento backup forward.
Step one: rebuild the signal map from the source database. The query above gives you the raw rows. The PHP loop below turns them into a CSV the merchandising lead could open in her own tools without waiting for us.
<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=mage_backup', 'reader', $pw);
$sql = <<<SQL
SELECT eao.option_id, ea.attribute_code, eao.additional_data
FROM eav_attribute_option eao
JOIN eav_attribute ea ON eao.attribute_id = ea.attribute_id
WHERE eao.additional_data IS NOT NULL
SQL;
$out = fopen('signal.csv', 'w');
fputcsv($out, ['option_id', 'attribute', 'return_rate', 'converts', 'reorder', 'profile']);
foreach ($pdo->query($sql) as $row) {
$p = @unserialize($row['additional_data']);
if (!is_array($p)) continue;
fputcsv($out, [
$row['option_id'],
$row['attribute_code'],
$p['return_rate'] ?? null,
$p['converts'] ?? null,
!empty($p['reorder']) ? 1 : 0,
$p['profile'] ?? '',
]);
}
fclose($out);
Step two: join those option IDs to the SKUs they had been bound to. In Magento that lives in catalog_product_entity_int (or _varchar, depending on the attribute backend type). That join gave us the missing map: which Shopify variant ID should carry which signal.
Step three: bulk-write Shopify metafields with the recovered data through the GraphQL Admin API. Three hours of scripting, eight minutes of API time, zero DNS change. By the end of day five, the merchandising lead had her badges back and the autumn calendar was usable again.
Rebuilding the signal on Shopify Plus the right way
The team wanted parity with Magento, but parity was the wrong target. The Magento setup was a workaround for an admin UI that did not support arbitrary structured data on option rows. Shopify supports it natively, through variant metafields. Shopify's metafield documentation is the canonical source. The structure we picked:
{
"namespace": "merchandising",
"key": "variant_signal",
"type": "json",
"owner_resource": "variant",
"value": "{\"return_rate\":0.034,\"converts\":0.082,\"reorder\":true,\"profile\":\"low_return_fit\"}"
}
One metafield per variant, typed as JSON, exposed to the storefront and to Shopify Flow. The merchandising report that used to live in a back-office PHP page now runs as a saved view inside the Shopify admin, filtered on metafield values. The frontend badge that swapped in icons is a Liquid snippet that reads the same payload.
{% assign signal = product.selected_variant.metafields.merchandising.variant_signal.value %}
{% if signal.profile == 'low_return_fit' %}
<span class="badge badge-fit">True to size</span>
{% endif %}
Two practical notes. First, set the metafield type to json, not json_string. The former is parsed by Liquid and queryable in the admin. The latter is opaque. Second, register the definition in Settings, Custom data, Variants before you bulk-write, otherwise the values land but no admin column appears and the team will assume the data is gone again.
A pre-flight checklist for any Magento 2 cutover
If you are sitting on Magento 2.4.4 or earlier, you are already past Adobe's software lifecycle window. You will migrate. Before any commercial tool touches your store, do these five things.
Schema diff against stock. Run SHOW CREATE TABLE on every eav_*, catalog_*, and sales_* table and diff against a clean Magento 2.4.x install. Any extra column is data the migrator will skip.
Inventory every serialized blob. Search the database for columns containing values that start with a:, O:, or s:. Those are PHP-serialized payloads. Each one is a small landmine.
Map custom modules to Shopify primitives. Custom EAV usually maps to variant or product metafields. Custom product types map to metaobjects. Custom checkout fields map to cart attributes. Do this mapping on paper before the cutover, not after.
Dry-run on a sandbox. Spin up a Shopify development store, run the migration, then run your three most-used merchandising reports. Anything that returns default across the board is a flatten.
Hold DNS for 72 hours past green tick. Migrator success is not merchandising success. The first 72 hours after a green tick are when silent flattens show up in the reports the team actually uses.
What to do this week
If you are mid-migration, run the schema diff today. If you are post-migration and something feels off in the new admin, dump the Magento backup and grep for serialized PHP. If you are pre-migration, write down every back-office report that does not have an obvious source in the standard Magento UI. Those reports are where the hidden columns live.
When we ran this rescue for the Gent client, the fix took less time than the diagnosis. The diagnosis took less time than the original developer's missing handover document would have. This is the work we do most often: legacy migration from Magento and the older PHP stacks onto something a small team can actually run. The serialized EAV trap is one of about a dozen we now check for by default.
The smallest thing you can do today: open one of your Magento attribute tables in a database client and look for an additional_data, custom_data, or extension_data column. If it exists and it is not empty, you have a story like this one waiting to happen.
Key takeaway
The data Magento shows in its admin is not the data your team relies on. Custom modules hide signals in columns no off-the-shelf migrator will read.
FAQ
Why does custom Magento data flatten during a Shopify migration?
Off-the-shelf migrators only read standard Magento columns. Anything a custom module added, such as extra columns or serialized blobs, is invisible to the mapping logic and gets dropped without a warning.
Can I recover the data after the migration is already live?
Yes, if you kept the Magento database backup. Read the source rows, join on SKU, and write the recovered values to Shopify variant metafields through the bulk API. No DNS rollback is required.
Should I use Shopify metafields or a third-party app for this kind of signal?
For per-variant signals, use metafields. They are native, queryable in Liquid and Flow, and survive theme changes. Apps are for behaviour, not for data structure that the merchandising team owns.
How do I audit a Magento 2 store before any migration?
Run SHOW CREATE TABLE on every eav_, catalog_, and sales_ table and diff against a stock 2.4.x install. Any extra column is data the migrator will skip. Then grep the dump for serialized PHP prefixes.