Magento 2.2 error: Unable to unserialize value

The problem is in /vendor/magento/framework/Serialize/Serializer/Json.php there is a function unserialize($string) which gives you a syntax error if the string is already serialized.

There is a workaround - you can check if string is serialized and then use serialize($string). Change unserialize to:

public function unserialize($string)
{
    /* Workaround: serialize first if is serialized */
    if($this->is_serialized($string))
    {
        $string = $this->serialize($string);
    }
    $result = json_decode($string, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
         throw new \InvalidArgumentException('Unable to unserialize value.');

    }
    return $result;
}

and add function to check if string is serialized:

function is_serialized($value, &$result = null)
{
    // Bit of a give away this one
    if (!is_string($value))
    {
        return false;
    }
    // Serialized false, return true. unserialize() returns false on an
    // invalid string or it could return false if the string is serialized
    // false, eliminate that possibility.
    if ($value === 'b:0;')
    {
        $result = false;
        return true;
    }
    $length = strlen($value);
    $end    = '';
    switch ($value[0])
    {
        case 's':
            if ($value[$length - 2] !== '"')
            {
                return false;
            }
        case 'b':
        case 'i':
        case 'd':
            // This looks odd but it is quicker than isset()ing
            $end .= ';';
        case 'a':
        case 'O':
            $end .= '}';
            if ($value[1] !== ':')
            {
                return false;
            }
            switch ($value[2])
            {
                case 0:
                case 1:
                case 2:
                case 3:
                case 4:
                case 5:
                case 6:
                case 7:
                case 8:
                case 9:
                    break;
                default:
                    return false;
            }
        case 'N':
            $end .= ';';
            if ($value[$length - 1] !== $end[0])
            {
                return false;
            }
            break;
        default:
            return false;
    }
    if (($result = @unserialize($value)) === false)
    {
        $result = null;
        return false;
    }
    return true;
}

After save fe. category without problem, You can restore class to default and there wont be such problem in future.


So, solution is write a UpdateData script in the own module which change the default serialized values to the JSON (like Magento does it in the CatalogRules module):

app/code/MageWorx/ShippingRules/Setup/UpgradeData.php

class UpgradeData implements UpgradeDataInterface
{
    /**
     * @var \Magento\Framework\App\ProductMetadata
     */
    protected $productMetadata;

    /**
     * @var MetadataPool
     */
    private $metadataPool;

    /**
     * @var \Magento\Framework\DB\AggregatedFieldDataConverter
     */
    private $aggregatedFieldConverter;

    /**
     * UpgradeData constructor.
     *
     * @param MetadataPool $metadataPool
     * @param ObjectManagerInterface $objectManager
     */
    public function __construct(
        MetadataPool $metadataPool,
        ObjectManagerInterface $objectManager,
        \Magento\Framework\App\ProductMetadata $productMetadata
    ) {
        $this->productMetadata = $productMetadata;
        $this->metadataPool = $metadataPool;
        if ($this->isUsedJsonSerializedValues()) {
            $this->aggregatedFieldConverter = $objectManager->get('Magento\Framework\DB\AggregatedFieldDataConverter');
        }
    }

    /**
     * @return bool
     */
    public function isUsedJsonSerializedValues()
    {
        $version = $this->productMetadata->getVersion();
        if (version_compare($version, '2.2.0', '>=') &&
            class_exists('\Magento\Framework\DB\AggregatedFieldDataConverter')
        ) {
            return true;
        }

        return false;
    }

Here we use the ObjectManager class because there is no such class \Magento\Framework\DB\AggregatedFieldDataConverter in the magento versions < 2.2.

Then initialize the update method:

/**
 * {@inheritdoc}
 */
public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
    $setup->startSetup();

    // 2.0.2 - module version compatible with magento 2.2
    if (version_compare($context->getVersion(), '2.0.2', '<') && $this->aggregatedFieldConverter) {
        // Convert each entity values
        $this->convertRuleSerializedDataToJson($setup);
        $this->convertZoneSerializedDataToJson($setup);
    }

    $setup->endSetup();
}

and method for conversion:

/**
 * Convert Zone metadata from serialized to JSON format:
 *
 * @param ModuleDataSetupInterface $setup
 *
 * @return void
 */
protected function convertZoneSerializedDataToJson(ModuleDataSetupInterface $setup)
{
    $metadata = $this->metadataPool->getMetadata(ZoneInterface::class);
    $this->aggregatedFieldConverter->convert(
        [
            new FieldToConvert(
                SerializedToJson::class,
                $setup->getTable(Zone::ZONE_TABLE_NAME),
                $metadata->getLinkField(),
                'conditions_serialized'
            ),
        ],
        $setup->getConnection()
    );
}

where:

  • ZoneInterface - entity interface, usually located at the Module/Vendor/Api/Data/
  • Zone::ZONE_TABLE_NAME - corresponding table name, like 'mageworx_shippingrules_zone' string
  • conditions_serialized - name of the field having a serialized data, which should be converted to JSON

To make it work it is necessary to have few important things:

  • Your interface should have corresponding model in the etc/di.xml:

    <preference for="MageWorx\ShippingRules\Api\Data\RuleInterface" type="MageWorx\ShippingRules\Model\Rule" />
    
  • Corresponding entity repository should be defined in the RepositoryFactory in the etc/di.xml (and this repository should exist):

    <type name="Magento\Framework\Model\Entity\RepositoryFactory">
        <arguments>
            <argument name="entities" xsi:type="array">
                <item name="MageWorx\ShippingRules\Api\Data\RuleInterface" xsi:type="string">MageWorx\ShippingRules\Api\RuleRepositoryInterface</item>
                <item name="MageWorx\ShippingRules\Api\Data\ZoneInterface" xsi:type="string">MageWorx\ShippingRules\Api\ZoneRepositoryInterface</item>
            </argument>
        </arguments>
    </type>
    
  • Your entity shoul be defined in the MetadataPool (in etc/di.xml):

    <type name="Magento\Framework\EntityManager\MetadataPool">
        <arguments>
            <argument name="metadata" xsi:type="array">
                <item name="MageWorx\ShippingRules\Api\Data\RuleInterface" xsi:type="array">
                    <item name="entityTableName" xsi:type="string">mageworx_shippingrules</item>
                    <item name="identifierField" xsi:type="string">rule_id</item>
                </item>
                <item name="MageWorx\ShippingRules\Api\Data\ZoneInterface" xsi:type="array">
                    <item name="entityTableName" xsi:type="string">mageworx_shippingrules_zone</item>
                    <item name="identifierField" xsi:type="string">entity_id</item>
                </item>
            </argument>
        </arguments>
    </type>
    
  • In corresponding resource model default serializer (class) should not be defined or should be instance of Magento\Framework\Serialize\Serializer\Json (as default). It's stored in the $this->serializer attribute of the resource model.

If something goes wrong during the load process of your model I'll recomend you to start debugging from resource model, especially from the magento/framework/Model/ResourceModel/AbstractResource.php class from method protected function _unserializeField(DataObject $object, $field, $defaultValue = null) where:

  • $object - should be instance of your model
  • $field - serialized field, like conditions_serialized

The following example is taken from the MageWorx Shipping Suite extension.