Paste the output of PHP's serialize() from a
database column, a session file or a WordPress
wp_options row and read it as JSON, print_r,
var_export or var_dump. The parser counts string lengths in
UTF-8 bytes like PHP does, so s:5:"café" works, and
when a search-and-replace has left the lengths wrong it finds the
real ends and offers to fix them. The parser is written in
JavaScript and runs in this tab, so no PHP executes and nothing is
uploaded.
O:5:"Order":5:{s:2:"id";i:1042;s:8:"customer";s:12:"Zoë Müller";s:5:"items";a:1:{i:0;a:3:{s:3:"sku";s:8:"CAFÉ-01";s:3:"qty";i:2;s:5:"price";d:4.5;}}s:4:"paid";b:1;s:4:"note";N;} {
"__class": "Order",
"id": 1042,
"customer": "Zoë Müller",
"items": [
{
"sku": "CAFÉ-01",
"qty": 2,
"price": 4.5
}
],
"paid": true,
"note": null
}
Look at s:12:"Zoë Müller": 10 characters, 12
bytes, because ë and ü take two bytes each in UTF-8. A parser
that counts characters gets this wrong and either fails or
cuts the string short.
| Token | Meaning |
|---|---|
| N; | null |
| b:1; | boolean, 1 or 0 |
| i:42; | integer (64-bit, kept exact even past JavaScript's 2^53) |
| d:0.5; | float, also INF, -INF and NAN. JSON has no way to write those, so they come out as strings |
| s:5:"café"; | string, length in bytes |
| a:2:{...} | array with 2 key/value pairs. Keys are i: or s: |
| O:5:"Order":3:{...} | object of class Order with 3 properties. Shown with a "__class" key |
| C:11:"ArrayObject":21:{...} | class with its own Serializable format. The payload is shown raw under "__serialized" |
| E:11:"Suit:Hearts"; | enum case (PHP 8.1+) |
| r:3; R:3; | reference to the 3rd value parsed (object reuse, and PHP &references). Resolved in the output; a loop prints *RECURSION* |
Property names carry their visibility in NUL bytes:
\0*\0email is protected, and
\0Customer\0tier is private to Customer. The
output uses print_r's convention, email:protected
and tier:Customer:private. Some database clients
drop those NUL bytes when you copy serialized data, which
breaks each affected length by two. The repair below handles
that, although the visibility information is lost.
The most common way to break serialized data is a plain SQL
REPLACE() during a domain move:
s:19:"http://example.com/" becomes
s:19:"https://example.org/", a 20-byte string
with a declared length of 19. unserialize() then
returns false with an "Error at offset" warning, and WordPress
falls back to default widget and theme settings without
telling you why.
When a length is off, this tool looks for the nearest
"; that is followed by something valid, shows
the result with a warning, and enables Fix
lengths, which rewrites only the wrong numbers in
your input. Check the result before writing it back, since
a string that itself contains "; can fool the
guess. For a real migration, use a
serialization-aware tool such as wp
search-replace.
WordPress also serializes values that are already serialized
strings, so a field can hold s:30:"a:1:{...}".
Tick Expand nested to decode those inner
layers in place. Base64-wrapped input (common in cookies and
some cache drivers) is detected and decoded first.
<?php
$input = 'a:2:{s:4:"name";s:5:"café";s:4:"tags";a:2:{i:0;s:1:"a";i:1;s:1:"b";}}';
// allowed_classes => false: objects come back as __PHP_Incomplete_Class
// instead of running a class's __wakeup()/__destruct(). Do this for any
// input you did not produce yourself.
$data = unserialize($input, ['allowed_classes' => false]);
// false is also a valid serialized value (b:0;), so check both
if ($data === false && $input !== serialize(false)) {
throw new RuntimeException('Invalid serialized data');
}
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
// The other direction. strlen() counts bytes, which is what s:N: uses.
echo serialize(['name' => 'café']), PHP_EOL; // a:1:{s:4:"name";s:5:"café";}
// WordPress: get_option() already unserializes; maybe_unserialize()
// is for values you read straight from the database. unserialize() returns false and raises an E_WARNING (E_NOTICE before PHP 8.3) on malformed input.
// Minimal serialize() for JSON-shaped data. The one rule people get
// wrong: s:N: is the length in UTF-8 bytes, not string.length.
const enc = new TextEncoder();
const isIntKey = (k) => /^(0|-?[1-9]\d*)$/.test(k);
function phpSerialize(v) {
if (v === null) return "N;";
if (typeof v === "boolean") return `b:${v ? 1 : 0};`;
if (typeof v === "number") return Number.isInteger(v) ? `i:${v};` : `d:${v};`;
if (typeof v === "string") return `s:${enc.encode(v).length}:"${v}";`;
const entries = Array.isArray(v) ? v.map((x, i) => [String(i), x]) : Object.entries(v);
return `a:${entries.length}:{` + entries.map(([k, x]) =>
(isIntKey(k) ? `i:${k};` : phpSerialize(k)) + phpSerialize(x)).join("") + "}";
}
console.log("café".length, enc.encode("café").length); // 4 5
console.log(phpSerialize({ name: "café", tags: ["a", "b"], price: 4.5 }));
// a:3:{s:4:"name";s:5:"café";s:4:"tags";a:2:{i:0;s:1:"a";i:1;s:1:"b";}s:5:"price";d:4.5;} Integers above 2^53 and floats in exponent form need more care; this covers JSON-shaped data.
# pip install phpserialize
import json
import phpserialize
raw = 'a:2:{s:4:"name";s:5:"café";s:4:"tags";a:2:{i:0;s:1:"a";i:1;s:1:"b";}}'
# phpserialize works on bytes; decode_strings gives you str back
data = phpserialize.loads(raw.encode("utf-8"), decode_strings=True)
print(data) # {'name': 'café', 'tags': {0: 'a', 1: 'b'}}
# PHP arrays come back as dicts; turn list-like ones into lists
print(json.dumps(phpserialize.dict_to_list(data["tags"]))) # ["a", "b"]
# Objects need object_hook, e.g. phpserialize.phpobject
print(phpserialize.dumps({"name": "café"}).decode("utf-8"))
# a:1:{s:4:"name";s:5:"café";} phpserialize is a third-party package on PyPI.
serialize() is PHP-specific and records types exactly: integers stay integers, floats keep their precision, and objects keep their class name. That class name is also why it is dangerous with untrusted input, since unserialize() can instantiate the class. For data that crosses a trust boundary or another language, use json_encode() instead.
Classes that implement __serialize() (PHP 7.4+) or the older Serializable interface write their own payload. ArrayObject is one example. The tool shows those payloads as they are, which may not match what print_r prints for the live object.
This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.
Usually a string length no longer matches its content, for example after a search-and-replace changed a URL inside serialized data. Paste the data here, and the tool finds the real string ends and enables Fix lengths to rewrite the numbers.
Bytes. s:5:"café" is correct because é takes two bytes in UTF-8. A serializer that counts characters breaks on any non-ASCII text, which makes it the most common bug in hand-written serializers.
As a JSON object with a "__class" key. Protected properties appear as name:protected and private ones as name:Class:private, the same convention print_r uses.
Not in PHP. unserialize() on untrusted input can instantiate arbitrary classes and trigger object injection. Use json_decode(), or at least pass ['allowed_classes' => false]. Parsing it here is safe because no PHP code runs.
Yes. Switch Mode to Serialize: JSON objects become PHP arrays with byte-correct lengths, numeric keys become integers like json_decode(..., true) does, and "__class" objects can be rebuilt as O: entries.
Once it is JSON, the JSON formatter can validate and fold it. For PHP array literals instead of serialize() output, use PHP array to JSON or go the other way with JSON to PHP array. If the value arrived Base64-encoded, the Base64 decoder shows what is inside.