In PHP, how do you read the string serialize() produces, such as a:2:{i:0;s:3:"foo";i:1;b:1;}?
answer
- type letter, colon, payload, semicolon
- s: length counts bytes
- a:count:{key;value;...}
- O: carries the class name
- Error at offset, then false
basics
~20 sEach 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.
solid answer
~30 sEvery value starts with a **one-letter tag**: `i:42;` is an int, `d:0.19;` a float, `b:1;` a bool, `N;` null, and `s:3:"foo";` a string whose number is its **length in bytes**. An array is `a:<count>:{...}` followed by alternating key and value entries, so `a:2:{i:0;s:3:"foo";i:1;b:1;}` is `[0 => "foo", 1 => true]`. An object is `O:<name length>:"<Class>":<property count>:{...}`, an enum case is `E:`, and `R:`/`r:` point back at a value already written. Because lengths are byte counts, hand-editing a serialized string, or a search-and-replace over a database dump, breaks it: `unserialize()` then emits an `E_WARNING` ("Error at offset ...") and returns `false`.
code
php · 5 lines<?php
$tariff = ['zone' => 'EU', 'rate' => 0.19, 'active' => true, 'tiers' => [10, 20]];
echo serialize($tariff), "\n";
// a:4:{s:4:"zone";s:2:"EU";s:4:"rate";d:0.19;s:6:"active";b:1;s:5:"tiers";a:2:{i:0;i:10;i:1;i:20;}}go deeper
Recall the tags i, d, b, s, N, a and O, and that the number after s: is a byte length. Be able to decode a short array payload aloud.
Explain property-name mangling for protected and private members, why search-and-replace breaks payloads, and how unserialize() signals failure with false plus a warning.
Diagnose a corrupted cache or legacy column from the raw string: truncation, wrong byte counts, stripped NUL bytes, or an unexpected class name in an O: entry.
Weigh keeping PHP-format payloads in shared storage at all: they couple stored data to PHP and to class names, which constrains migrations and other consumers.
## What serialize() produces `serialize()` turns a PHP value, including arrays and objects, into a string in **PHP's own format**, and `unserialize()` turns that string back into a value. The format is not JSON and no other language's JSON parser reads it. You meet it in cache entries, queue payloads and database columns written by older applications, and PHP's default session serializer (`session.serialize_handler = php`) wraps each session variable in the same encoding, so being able to read one by eye is a practical debugging skill. The grammar is small: **a tag letter, a colon, the payload, and a terminator** (`;` for scalars, `}` for containers). ## The tags | Value | Example value | Serialized form | |---|---|---| | null | `null` | `N;` | | bool | `true` / `false` | `b:1;` / `b:0;` | | int | `42` | `i:42;` | | float | `0.19` | `d:0.19;` | | string | `"EU"` | `s:2:"EU";` | | array | `['zone' => 'EU']` | `a:1:{s:4:"zone";s:2:"EU";}` | | object | a `stdClass` with one property | `O:8:"stdClass":1:{s:4:"zone";s:2:"EU";}` | | enum case (8.1+) | `Suit::Hearts` | `E:11:"Suit:Hearts";` | Less common tags: - **`R:` and `r:`** refer back to a value already written in the same payload, which is how shared objects and PHP references survive a round trip. - **`C:`** is the legacy format of classes implementing the `Serializable` interface; the payload inside the braces is whatever that class's `serialize()` method returned. - **`S:`** (uppercase) is an old escaped-string form; unserializing it is deprecated as of PHP 8.4. ## Arrays and objects An array is written as `a:<element count>:{` followed by key/value pairs and a closing `}`. Keys are only ever `i:` or `s:` entries, because PHP array keys are ints or strings. There is no separator between pairs: every entry ends with its own `;` or `}`, so the parser always knows where the next one starts. An object is `O:<length of class name>:"<ClassName>":<property count>:{...}`. Inside the braces the property names are **mangled** by visibility: - a public property is written by name: `s:4:"zone";` - a protected one is prefixed with NUL, `*`, NUL: `"\0*\0zone"` - a private one is prefixed with NUL, the declaring class name, NUL: `"\0Tariff\0zone"` Those NUL bytes are part of the string and are counted in its length. A storage layer or tool that strips NUL bytes, such as a text column in some databases or a careless copy-paste, silently corrupts the payload. A binary column, or base64 around the payload, avoids that. ## Lengths count bytes, not characters The number in `s:<n>:` is the string's length in **bytes**. In UTF-8, `"café"` is five bytes, so it serializes as `s:5:"café";`. This is the root of the most common real-world breakage: 1. A migration runs search-and-replace over a database dump, changing `http://old.example` to `https://new.example`. 2. The replacement is longer, but the `s:` counts inside serialized columns still describe the old text. 3. `unserialize()` reads the declared number of bytes, finds something other than `";` where it expects the terminator, and gives up. The fix is to change serialized data only through PHP: `unserialize()`, modify the value, `serialize()` again. ## Reading a payload step by step Take `a:2:{i:0;s:3:"foo";i:1;b:1;}`: 1. `a:2:{` opens an array that declares two elements. 2. `i:0;` is the first key, the int `0`; `s:3:"foo";` is its value, a three-byte string. 3. `i:1;` is the second key; `b:1;` is its value, `true`. 4. `}` closes the array, so the value is `[0 => 'foo', 1 => true]`. If the braces never close, the payload was truncated, typically by a column that is too short for it. ## How unserialize() reports a bad payload - On a malformed string it returns **`false`** and emits an **`E_WARNING`** of the form `Error at offset X of Y bytes`. PHP 8.3 promoted that diagnostic from `E_NOTICE`. - Since PHP 8.3 it also warns (`Extra data starting at offset ...`) when valid data is followed by unconsumed bytes, but still returns the decoded value. - `unserialize('b:0;')` legitimately returns `false` too. To tell a stored `false` from a failure, compare the input with `serialize(false)` before trusting a `false` result, or watch for the warning. Knowing the tags lets you read a stuck cache entry, spot a truncated column (the braces never close), or see at a glance which class an `O:` payload will try to instantiate.
- Why does a search-and-replace across a database dump so often break serialized columns?Every `s:` entry records its length in bytes. Replacing a URL or a name with one of a different length leaves the recorded count wrong, so `unserialize()` reads the wrong number of bytes, misses the `";` terminator, warns and returns `false`. Change such data by unserializing, editing the value, and serializing it again.
- How do private and protected property names appear inside an O: payload?They are mangled with NUL bytes: a protected `$rate` is stored as `"\0*\0rate"` and a private one as `"\0ClassName\0rate"`, with the NULs counted in the `s:` length. Public properties appear by plain name. Anything that strips NUL bytes in transit corrupts those entries.
saying these in an interview costs you the question
- The s: number counts characters, so UTF-8 text can be edited freely
- unserialize() throws an exception when the string is malformed
- serialize() output is JSON that any language can parse
- A false result from unserialize() always means the payload was corrupt
- Serialized data can be fixed by hand as long as the quotes still match