PHPSerialization

Serialization

Serialization is the process of turning an in-memory PHP value — including arrays, objects, and their full nested structure — into a single string that can be stored in a file, cached, or sent somewhere, and later reconstructed back into an equivalent value. PHP has its own native serialization format, produced by serialize() and reversed by unserialize(), predating JSON's popularity and still showing up in session storage, caches, and legacy codebases.

serialize() and unserialize(): the basics

serialize() accepts almost any PHP value and returns a string encoding both the value's type and its content. unserialize() takes that string and reconstructs the original value.

Round-tripping an array

PHP
<?php
$data = ['name' => 'Firoz', 'age' => 30, 'active' => true];

$serialized = serialize($data);
echo $serialized, "\n";

$restored = unserialize($serialized);
print_r($restored);
a:3:{s:4:"name";s:5:"Firoz";s:3:"age";i:30;s:6:"active";b:1;}
Array
(
    [name] => Firoz
    [age] => 30
    [active] => 1
)
Reading the serialized format

The format is dense but readable once its pattern is known: each value is written as a single letter for its type, followed by length or value information. a:3:{...} means "an array with 3 entries." s:4:"name" means "a string of 4 characters: name." i:30 means "an integer, 30." b:1 means "a boolean, true." This length-prefixing is exactly why serialized strings are fragile to hand-edit — changing "Firoz" to "Fir" without also updating the s:5 length prefix in front of it corrupts the whole string and makes unserialize() fail.

Serializing an object

PHP
<?php
class Point
{
    public function __construct(
        public float $x,
        public float $y,
    ) {
    }
}

$point = new Point(3.5, 7.0);
$serialized = serialize($point);
echo $serialized, "\n";

$restored = unserialize($serialized);
echo $restored->x + $restored->y, "\n";
O:5:"Point":2:{s:1:"x";d:3.5;s:1:"y";d:7;}

Notice the class name Point is embedded directly in the serialized string (the O:5:"Point" prefix). Reconstructing the object requires that exact class to exist and be loaded (autoloaded or already required) at the moment unserialize() runs — this detail becomes important below.

Controlling serialization: magic methods

Sometimes an object shouldn't be serialized exactly as-is — maybe it holds a database connection or file handle that can't survive being turned into a string, or it needs to recompute some derived state after being restored. PHP offers two generations of hooks for this. The older pair, __sleep() and __wakeup(), has been available since early PHP: __sleep() runs before serialization and returns an array of property names to actually include; __wakeup() runs right after unserialize() reconstructs the object, as a chance to re-open any resources or re-validate state.

__sleep and __wakeup

PHP
<?php
class DatabaseLogger
{
    private $connection;
    public string $tableName;

    public function __construct(string $tableName)
    {
        $this->tableName = $tableName;
        $this->connection = 'a live connection resource, not serializable';
    }

    public function __sleep(): array
    {
        // Only persist tableName; skip the connection entirely
        return ['tableName'];
    }

    public function __wakeup(): void
    {
        // Re-establish a fresh connection after restoring
        $this->connection = 'freshly reconnected';
    }
}

$logger = new DatabaseLogger('events');
$restored = unserialize(serialize($logger));
echo $restored->tableName, "\n";
events

PHP 7.4 introduced a more capable replacement pair, __serialize() and __unserialize(), which take full control: __serialize() returns an array representing whatever state should be saved (not limited to existing property names), and __unserialize() receives that array back and is responsible for rebuilding the object's state from it. When both generations are defined, PHP prefers the newer pair.

__serialize and __unserialize

PHP
<?php
class DatabaseLogger
{
    private $connection;
    public string $tableName;

    public function __construct(string $tableName)
    {
        $this->tableName = $tableName;
        $this->connection = 'a live connection resource';
    }

    public function __serialize(): array
    {
        return ['table' => $this->tableName];
    }

    public function __unserialize(array $data): void
    {
        $this->tableName = $data['table'];
        $this->connection = 'freshly reconnected';
    }
}

$restored = unserialize(serialize(new DatabaseLogger('events')));
echo $restored->tableName, "\n";
events
unserialize() on untrusted input is a critical security risk
Calling `unserialize()` on a string that came from outside your own system — user input, an uploaded file, a cookie, a value from a third party — is one of the most dangerous things a PHP application can do. Because a serialized string can encode arbitrary objects, an attacker who controls the input can craft a payload that instantiates classes already present in the application (this is called PHP Object Injection) and trigger their magic methods — `__wakeup()`, `__destruct()`, `__unserialize()`, and others — with attacker-chosen property values. Depending on what those classes do in their magic methods (write files, run queries, make network calls), this can escalate all the way to remote code execution, and there is a long history of real-world exploits built exactly this way. Never call `unserialize()` on data that didn't originate from your own trusted server-side code. If a string does need to be `unserialize()`d, PHP's `allowed_classes` option restricts which classes may be instantiated, which meaningfully reduces (but does not fully eliminate) the risk. For any data crossing a trust boundary — user sessions aside — prefer JSON: `json_decode()` only ever produces plain arrays, scalars, and `stdClass` objects, none of which have magic methods an attacker can hijack.

Restricting which classes unserialize() may instantiate

PHP
<?php
// Still risky, but meaningfully safer than the default of allowing any class
$restored = unserialize($input, ['allowed_classes' => [Point::class]]);

// Safer still: don't use PHP serialization for untrusted data at all
$restored = json_decode($input, true, 512, JSON_THROW_ON_ERROR);
When PHP's native format still makes sense

Despite the risk with untrusted input, serialize()/unserialize() remain reasonable choices for data that never leaves your own trusted infrastructure and never crosses a language boundary — for example, caching a computed PHP object in an internal cache layer that only your own application writes to and reads from. JSON is the safer and more portable default for anything else: it's language-agnostic, human-readable, has no notion of "classes" for an attacker to abuse, and is understood natively by virtually every API client and browser.

  • serialize() encodes a PHP value, including objects, into a single string; unserialize() reverses it.

  • The format embeds type letters and explicit length prefixes, which makes hand-editing a serialized string fragile.

  • __sleep()/__wakeup() are the older hooks for customizing what gets saved and what happens after restoring.

  • __serialize()/__unserialize() (PHP 7.4+) are the modern, more flexible replacement and take priority when both exist.

  • unserialize() on untrusted input enables PHP Object Injection attacks — treat it as a critical security boundary.

  • Prefer JSON for any data that is untrusted, crosses a language boundary, or leaves your own infrastructure.

Note
Session data in PHP is serialized internally using a similar but not identical format controlled by the `session.serialize_handler` setting — this is handled automatically by the session extension and is a separate concern from calling `serialize()` directly in application code.
Tip
Treat `unserialize()` the way you'd treat `eval()` on user input — as something that should essentially never happen. If existing code calls `unserialize()` on anything that could conceivably originate from a request, cookie, or file upload, that's worth flagging and migrating to JSON as a priority, not just a style preference.