skip to content

Serialized PHP Values

serialize and unserialize turn PHP values, objects included, into PHP's own string format, shaped by __serialize and __unserialize. Interviewers probe allowed_classes and when JSON is safer.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In PHP, when should you store a value with serialize() rather than json_encode(), and what does each round trip lose?

level: middleimportance: must knowfreq 58%

answer

  1. who reads it back, and in what language
  2. class names and private properties
  3. stdClass or array after json_decode
  4. binary-safe vs UTF-8 only
  5. decode instantiates classes

basics

~20 s

Use serialize() only for PHP-to-PHP storage you control, because it restores exact types, classes, private properties and shared references. Use json_encode() for anything another program or an untrusted party touches: portable and inert, but classes and non-public state are lost.

solid answer

~40 s

`serialize()` is a **PHP-only, lossless** format: ints stay ints, floats stay floats, objects come back as instances of their class with protected and private properties, enum cases come back as the same case, shared references survive, and strings can hold arbitrary binary bytes. The price is coupling: the payload names your classes, so a rename or refactor can break old entries, and `unserialize()` instantiates those classes and runs their hooks. `json_encode()` is **portable and inert**: other languages read it, and `json_decode()` only ever builds arrays, `stdClass`, scalars and null. It loses class identity, skips non-public properties, and fails on invalid UTF-8. So: `serialize()` for a PHP-internal cache you write and read yourself; JSON for APIs, cross-language queues and anything that crosses a trust boundary.

code

php · 16 lines
php
<?php
declare(strict_types=1);

final class Tariff
{
    public function __construct(
        public readonly string $zone,
        private readonly float $rate,
    ) {}
}

$t = new Tariff('EU', 0.19);

var_dump(unserialize(serialize($t)));   // object(Tariff) with zone and the private rate
echo json_encode($t), "\n";               // {"zone":"EU"}  -- private $rate skipped
var_dump(json_decode(json_encode($t)));  // object(stdClass) with zone only

go deeper

for a junior

Know that serialize() is PHP-only and restores classes, while JSON is portable and comes back as arrays or stdClass.

for a middle

Explain what each round trip loses: class identity, non-public properties, binary strings and references for JSON; portability and decoupling for serialize().

for a senior

Show the production judgment: serialized objects in caches and queues break across deploys, and anything outside your control belongs in JSON.

for a principal

Frame the long-term cost: a serialized-object store ties data to class layout and to PHP, so set a policy on which stores may hold it and for how long.

## Two very different formats behind two similar calls Both `serialize()` and `json_encode()` turn a PHP value into a string, so they look interchangeable. They are not. **`serialize()`** writes PHP's own format, designed so `unserialize()` can rebuild the *exact* value, objects included. **`json_encode()`** writes JSON, a language-neutral format that knows only objects, arrays, strings, numbers, booleans and null. The choice therefore turns on two questions: *who reads the data back*, and *what must survive the trip*. ## What survives each round trip | Aspect | `serialize()` / `unserialize()` | `json_encode()` / `json_decode()` | |---|---|---| | array shape | any array restored exactly, keys included | a 0..n-1 list becomes a JSON array, any other array a JSON object, and an empty array is always `[]` | | object class | restored as the same class | lost: `stdClass`, or an array with `assoc = true` | | protected / private properties | kept | skipped unless the class shapes its own output | | enum cases | restored as the case | a backed enum encodes as its value, then stays a scalar | | shared references, cycles | preserved via back-references | not representable | | binary strings | any bytes | invalid UTF-8 makes encoding fail by default | | readable by other languages | no | yes | | runs your code on decode | yes: class hooks, autoloader, destructors | no | A few of these deserve a sentence each: - **Class identity.** A `Tariff` object serialized with `serialize()` comes back as a `Tariff` with its constructor *not* called and its state restored. Through JSON it comes back as `stdClass` or an array, and rebuilding the object is your job. - **Visibility.** `json_encode()` skips protected and private properties of plain objects, so an object whose state is private encodes to `{}`. `serialize()` writes every property, mangling non-public names with NUL bytes. - **Encoding.** A string holding raw bytes, such as a hash digest or a compressed blob, serializes fine; `json_encode()` rejects invalid UTF-8 unless you ask it to substitute or ignore. ## What serialize() costs 1. **Coupling to code.** The payload carries class names and property names. Renaming a class, renaming a property or changing a property's type can make old payloads fail or come back half-populated. 2. **Coupling to PHP.** No other runtime reads the format, so it cannot be the contract between services written in different languages. 3. **Code runs on decode.** `unserialize()` may autoload classes and runs their `__unserialize()` or `__wakeup()`, and their destructors run later. That is harmless when the bytes are ones your own code wrote, and dangerous when they are not. The PHP manual's own advice is to use JSON for anything a user can supply. 4. **Some values cannot be serialized at all.** Closures, generators, fibers, `PDO` handles and similar internal objects throw `Exception` with the message `Serialization of 'Closure' is not allowed` (with the matching class name). ## A decision rule - **Choose `serialize()`** when PHP writes and PHP reads, the storage is yours, and exact reconstruction matters: caching a computed tariff table of value objects between requests, or a job payload consumed only by your own PHP workers. - **Choose JSON** when anything else is involved: a browser, a mobile client, a service in another language, a column other teams query, a cookie or hidden form field, or any payload that could have been altered by someone outside your code. - **Choose neither blindly for long-lived data.** For data that outlives a deploy cycle, prefer an explicit shape (arrays of scalars, or JSON with a version field) over serialized objects, so a refactor does not orphan the store. ## A worked example: caching a tariff table Say each request needs a table of `Tariff` value objects computed from pricing rules. Cached with `serialize()`, the table comes back as real `Tariff` instances in one call, private state included, but every entry is tied to the current shape of the class. Cached as JSON, the table survives refactors and can be inspected with any tool, but reading it back means mapping each decoded array to a `Tariff` through a factory method. Both are defensible; the deciding questions are how often the class changes and whether anything other than your PHP code will ever read the entry. ## Where interviewers push The follow-ups usually probe the edges: what `json_decode()` returns without `assoc`, why a private-property object encodes to `{}`, how a stored `false` is ambiguous with `unserialize()`'s failure value, and why "just add `allowed_classes`" does not make untrusted input safe. A good answer names the concrete loss for each format rather than calling one of them "better".

  • What does json_decode() give you back for a JSON object, and how do you get your class again?
    By default a `stdClass` instance; with the `assoc` argument set to `true`, an associative array. Neither is your class. To get a `Tariff` back you map the decoded data yourself, for example through a named constructor such as `Tariff::fromArray()`, which also gives you a place to validate the input.
  • Your own PHP workers read a queue. Is serialize() then always fine for the payloads?
    Only if nothing outside your code can write to that queue and the payload classes are stable across deploys. A worker on the new release may read jobs queued by the old one, so renamed or retyped classes break in flight. Many teams send arrays or JSON with a version field for that reason.
  • Why does json_encode() return {} for an object whose properties are all private?
    For a plain object, `json_encode()` writes only public properties, and non-public ones are skipped. An object with only private state therefore encodes as an empty JSON object. `serialize()` would have written every property with mangled names.

saying these in an interview costs you the question

  • serialize() and json_encode() are interchangeable apart from output size
  • json_decode() restores objects as instances of their original class
  • serialize() output can be read by a service written in another language
  • unserialize() is inert, like json_decode(), and never runs class code
  • json_encode() includes private properties of plain objects
open as a page

In PHP, how do you read the string serialize() produces, such as a:2:{i:0;s:3:"foo";i:1;b:1;}?

level: juniorimportance: should knowfreq 38%

basics

~20 s

Each value is a type tag plus payload: i:42; is an int, s:3:"foo"; a string with its byte length, a:2:{...} an array with its element count, O: an object with its class name; b:, d: and N; cover bool, float, null.

open as a page

In PHP 8.5, how do __serialize() and __unserialize() differ from __sleep() and __wakeup(), and which should a new class implement?

level: middleimportance: should knowfreq 45%

basics

~20 s

__sleep() returns property names to keep and __wakeup() runs after they are restored; __serialize() returns any array and __unserialize() rebuilds the object from it. New classes should use __serialize()/__unserialize(): the older pair is soft-deprecated in PHP 8.5.

open as a page

In PHP, what do unserialize()'s allowed_classes and max_depth options change, and what does each leave unprotected?

level: seniorimportance: should knowfreq 40%

basics

~10 s

allowed_classes limits which classes unserialize() instantiates; any other class becomes __PHP_Incomplete_Class. max_depth caps nesting, default 4096 from unserialize_max_depth. Neither makes untrusted input safe: listed classes still run their hooks, and size is not bounded.

open as a page

After a PHP deploy renames or retypes properties of a class whose objects sit serialized in a cache, what does unserialize() do with the old payloads?

level: seniorimportance: should knowfreq 30%

basics

~10 s

unserialize() assigns stored properties by name: an undeclared key becomes a dynamic property (deprecated since 8.2, Error on readonly classes), a new typed property stays uninitialized, and a wrongly typed value throws TypeError.

open as a page

In PHP, how does var_export() let you cache a computed tariff table as a PHP file, and which values can it not export?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

var_export($value, true) returns PHP source for the value; write '<?php return ' . that . ';' to a file and require it later. Scalars, arrays, stdClass and enums work; cycles and resources do not, and other objects need __set_state().

open as a page