Magento 2 "Unable to Unserialize Value. Error: Syntax Error": The Complete Fix
The Magento 2 "Unable to unserialize value. Error: Syntax error" almost always means an old PHP serialize() value is sitting where Magento now expects JSON, usually after an upgrade or a mismatched database restore. This guide fixes it fix-first for 2.4.4 to 2.4.9: flush the cache, read the stack trace to find the table, then convert or clear the one bad row safely.
A page or a CLI command dies with Error: Unable to unserialize value, or the fuller Error: Unable to unserialize value. Error: Syntax error, and nothing in the message says which value or which table. The cause is almost always a value stored in the old PHP serialize() format, or a corrupted blob, sitting where Magento now expects JSON. This guide fixes it fix-first for Magento 2.4.4 to 2.4.9: flush the cache, read the trace, then convert or clear the one bad row.
What the error actually means
Magento 2.2 replaced PHP's native serialize() with JSON for values it stores in the database and the cache. Since then, the class that decodes those values is Magento\Framework\Serialize\Serializer\Json. Its unserialize() method runs json_decode() and, if the string is not valid JSON, throws an exception. That is the exact source of the message you are looking at:
<?php
// vendor/magento/framework/Serialize/Serializer/Json.php (illustrative, do not edit)
public function unserialize($string)
{
$result = json_decode($string, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \InvalidArgumentException('Unable to unserialize value. Error: ' . json_last_error_msg());
}
return $result;
}
When the value is a leftover PHP-serialized string such as a:2:{s:4:"name";s:3:"foo";}, json_decode() cannot parse it, json_last_error_msg() returns Syntax error, and you get Unable to unserialize value. Error: Syntax error. The same happens when a blob was truncated or corrupted, for example by a partial import or a bad copy between environments. So the fix is never to make the decoder more forgiving. It is to put valid JSON where Magento reads it.
Three situations produce these bad values on 2.4.4 to 2.4.9:
- Upgrading from an older line where some rows were written by
serialize()and a data patch never converted them. - Restoring a database dump from a different Magento minor onto a newer codebase, so the code expects JSON but the data is still serialized.
- A third-party module that still calls
serialize()directly and writes the result into a column Magento later reads with the JSON serializer.
Flush the cache before you touch the database
Start with the move that changes no data. Magento caches serialized config and layout blobs, and a poisoned cache entry throws the identical error even when every database row is already valid JSON. Clear it and retry:
bin/magento cache:flush
If your cache backend is Redis, cache:flush handles it, but if you suspect a stuck entry you can flush the specific Redis database Magento uses. Find the database number in app/etc/env.php, then flush only that one so you do not wipe sessions or other tenants on a shared Redis:
# see which Redis db the default cache maps to
grep -A6 "'cache'" app/etc/env.php
# flush only that db number (example: db 1), never a blind FLUSHALL
redis-cli -n 1 FLUSHDB
# then regenerate
bin/magento cache:flush
If the error is gone after this, it was a cache problem and you are done. If it comes straight back, the bad value is in the database, and the next step is to find out which one.
Read the stack trace to find the culprit
The error alone is useless, but the trace under it names the class that called unserialize(), and that tells you where the bad value lives. Pull it from the logs:
tail -n 60 var/log/exception.log
tail -n 60 var/log/system.log
# or, on a hard 500, the report id printed on screen
cat var/report/<report-id>
A typical trace reads like this, and the second and third lines are the ones that matter:
InvalidArgumentException: Unable to unserialize value. Error: Syntax error
#0 .../Config/Data.php: Magento\Framework\Serialize\Serializer\Json->unserialize()
#1 .../App/Config/ScopePool.php: ...
#2 .../Model/Config/Backend/Serialized.php: ...
Map the caller to a location. These four are the common ones.
A core_config_data row
Callers under Config\Backend\Serialized or the config loader point at core_config_data. This is the most frequent case: a payment method, a shipping carrier setting like table rates, or a module's grouped config that was stored serialized. The path in the trace, or the module name, tells you which row.
A customer or quote EAV attribute
Callers under a customer, address, or quote resource model point at an EAV value table. A multi-select or a serialized custom attribute written by an old extension is the usual source.
The flag table
Callers under Flag or a class that saves state (an indexer, an import, analytics) point at the flag table. These rows hold a single serialized blob per flag_code, and an interrupted job can leave a truncated one.
A module's own table
If the caller is a third-party namespace, the bad value is in that module's table, written by its own code. Fix it the same way, in its column.
Back up, then convert the bad row to JSON
Once you know the table, the durable fix is to rewrite the PHP-serialized value as JSON. For a config row the simplest approach is to open that setting in the admin and click Save Config, because saving runs the value back through the JSON serializer and rewrites it correctly. When the setting is hard to reach, or the value sits in a table with no admin form, convert it with a small bootstrap script.
Before you change a single row, copy the whole table. A serialized-to-JSON conversion is one UPDATE away from destroying data if you target the wrong row or the detection is wrong. Run CREATE TABLE core_config_data_backup AS SELECT * FROM core_config_data; (swap in your table name), and never open a serialized blob in a text editor to hand-edit it. Serialized strings encode byte lengths, so changing one character breaks the whole value.
Back up the table:
CREATE TABLE core_config_data_backup AS SELECT * FROM core_config_data;
Then run a script from the Magento root. It reads the suspected row, detects whether the value is PHP-serialized with @unserialize(), and if so rewrites it as JSON through the framework serializer. Point $path at the config path from your trace:
<?php
// convert_serialized.php, in the Magento root. Back up the table first.
use Magento\Framework\App\Bootstrap;
require __DIR__ . '/app/bootstrap.php';
$bootstrap = Bootstrap::create(BP, $_SERVER);
$om = $bootstrap->getObjectManager();
/** @var \Magento\Framework\App\ResourceConnection $resource */
$resource = $om->get(\Magento\Framework\App\ResourceConnection::class);
$conn = $resource->getConnection();
/** @var \Magento\Framework\Serialize\Serializer\Json $json */
$json = $om->get(\Magento\Framework\Serialize\Serializer\Json::class);
$table = $resource->getTableName('core_config_data');
$path = 'carriers/tablerate/condition_name'; // the path from your stack trace
$row = $conn->fetchRow(
$conn->select()->from($table)->where('path = ?', $path)
);
if (!$row) {
echo "No row for {$path}\n";
exit;
}
$value = (string) $row['value'];
// is it valid JSON already? then leave it alone
json_decode($value);
if (json_last_error() === JSON_ERROR_NONE) {
echo "Row is already valid JSON. No change.\n";
exit;
}
// try PHP serialize. b:0; legitimately unserializes to false, so allow it
$restored = @unserialize($value);
if ($restored === false && $value !== 'b:0;') {
echo "Value is neither JSON nor PHP-serialized. Inspect it by hand.\n";
exit;
}
$newValue = $json->serialize($restored);
$conn->update(
$table,
['value' => $newValue],
['config_id = ?' => (int) $row['config_id']]
);
echo "Converted config_id {$row['config_id']} to JSON: {$newValue}\n";
Run it, then flush the cache:
php convert_serialized.php
bin/magento cache:flush
The script is deliberately narrow: it converts one path, checks that the value is not already JSON, and refuses anything it cannot recognise as PHP-serialized. When the offending value is in an EAV or a module table, change $table, the lookup column, and the primary key column to match. If you are working through a broader set of post-upgrade failures, this one often travels with the traps covered in the five universal Magento 2.4.9 upgrade traps.
The flag table case, clear the stale row
The flag table deserves its own step because it is both a common culprit and the safest to fix. Each row is a piece of regenerable state keyed by flag_code: the config snapshot, indexer state, import progress, analytics data. When a job is killed midway, it can leave a truncated or half-written blob that later fails to decode. Because Magento rebuilds these on demand, you can delete the single bad row instead of converting it. Inspect first:
SELECT flag_id, flag_code, LENGTH(flag_data) AS len
FROM flag
ORDER BY len DESC
LIMIT 20;
Back up the table, then delete only the specific flag named in your trace. Do not truncate the whole table:
CREATE TABLE flag_backup AS SELECT * FROM flag;
-- example: a stale config snapshot left by an interrupted save
DELETE FROM flag WHERE flag_code = 'system_config_snapshot';
Flush the cache afterwards. Magento writes a fresh, valid row the next time that feature runs. This is the right move for the config snapshot, indexer flags, and import or export progress rows, all of which are caches of state rather than primary data.
The supported migration path
Historically, the serialize-to-JSON conversion was not something you did by hand. When you upgraded across the 2.2 boundary, bin/magento setup:upgrade ran data patches, and the framework shipped converters (the SerializedToJson upgrade classes) that walked the known tables and rewrote serialized values. On some lines the data phase was invoked as bin/magento setup:db-data:upgrade after the schema phase. An interrupted or skipped upgrade leaves exactly the kind of orphaned serialized rows this error complains about.
# the phase that runs data patches, including any serialize-to-json converters
bin/magento setup:upgrade
bin/magento setup:db-data:upgrade # older lines split the data phase out
bin/magento cache:flush
On a current 2.4.4 to 2.4.9 install there is no core command that scans for stray serialized values, because core assumes the conversion already happened. That is why the practical fix for a single bad row is the targeted convert-or-clear approach above, not a global migration command. If the failure surfaces during reindexing rather than page load, confirm the indexers are healthy with the Magento 2 reindex and index management guide, since a stuck indexer can both cause and mask a bad flag row.
Prevention
Most of these incidents are avoidable with two habits:
- Match the dump to the codebase. Never restore a database from a different Magento minor onto a newer or older codebase and expect it to run. If you must move data across versions, restore onto the matching version first, then run
bin/magento setup:upgradeto migrate it before switching code. - Do not hand-edit serialized data. PHP-serialized strings carry byte-length prefixes, so editing a value in phpMyAdmin or a SQL client corrupts the whole blob and produces the very error you are trying to fix. Change these values through admin or a script that reserializes them.
- Audit third-party modules. If the trace repeatedly names one extension, that module is writing serialized data into a JSON column. Report it to the vendor and keep the conversion script handy until it is patched.
Frequently asked questions
What does "Unable to unserialize value. Error: Syntax error" actually mean?
Magento tried to JSON-decode a stored value and it was not valid JSON. The "Syntax error" part is the message from PHP's json_last_error_msg(). The value is almost always old PHP serialize() data left in a table, or a truncated blob, sitting where the JSON serializer expects clean JSON.
Which table holds the bad value?
Read the stack trace under the error. The class that called unserialize() tells you: a Config\Backend\Serialized caller means core_config_data, a Flag caller means the flag table, a customer or quote resource means an EAV value table, and a third-party namespace means that module's own table.
Why did this appear right after an upgrade?
Because a data patch that should have converted serialized values to JSON never ran, or ran incompletely. This happens when an upgrade was interrupted, or when a database dump from an older minor was restored onto a newer codebase without running bin/magento setup:upgrade against it.
Can I just delete the row?
Only if it is regenerable state, which the flag table is. Delete a single flag_code row and Magento rebuilds it. For core_config_data or a module table, deleting loses the setting, so convert the value to JSON instead, or re-save it through admin.
Is it safe to edit the serialized string by hand in the database?
No. PHP-serialized strings store the byte length of every element, so changing one character makes the length wrong and the whole value unreadable. Always reserialize through code or re-save through admin. Back up the table before any change.
The error is gone after cache:flush. Is it really fixed?
If it does not come back, yes, it was a poisoned cache entry rather than a bad database row. If it returns on the next request or the next reindex, the value is in the database and you need to find and convert the offending row.
Does this differ between Magento 2.4.4 and 2.4.9?
No. The JSON serializer and this error behave the same across 2.4.4 to 2.4.9. There is no core command on any of these versions that scans for stray serialized values, so the targeted convert-or-clear fix applies to all of them.
A third-party module keeps causing this. What now?
The module is writing serialized data into a column Magento reads as JSON. Report it to the vendor with the stack trace, and keep converting its rows with the script until a patched version ships. Do not patch vendor code in place; the change will vanish on the next update.
Cannot pin down the bad value? When the trace is ambiguous or the same error keeps returning across environments, a short look at the live database finds the exact row fast. I run a $499 audit that locates and converts every offending value safely. See services or hire me.
Get a Magento developer on it