Why WordPress Still Uses PHP's Serialize Format in 2026
Open a WordPress database and look inside wp_options or a plugin's
postmeta rows, and you'll find values that look nothing like JSON - dense strings full of
a:3:{...} and s:5:"hello".
To anyone who learned web development after JSON became the default, this looks like a strange, almost
antiquated choice. It isn't an oversight, and it isn't something a plugin update quietly fixes someday. It's
PHP's native serialization format, it predates JSON's dominance in the PHP ecosystem, and WordPress is stuck
with it for reasons that are less about nostalgia and more about the practical cost of ever changing it.
What's Actually Sitting in That Column
PHP's serialize() function turns a value into a length-prefixed
text representation. A string becomes s:5:"hello"; - the
5 is the exact byte length of what follows. An integer becomes
i:42;. An array becomes something like
a:2:{i:0;s:3:"foo";i:1;s:3:"bar";}, where the
a:2 announces exactly two key/value pairs, followed by each key
and value serialized in turn. It's a completely mechanical, self-describing format - every piece announces its
own type and, for strings, its own exact length before the content appears.
That length-prefixing is the single detail that explains almost everything else about how this format behaves, both its usefulness and its most common failure mode, which is why it's worth sitting with for a moment before getting to the WordPress-specific part.
Why Not Just Use JSON? (The Format Predates the Question)
PHP's serialize() has existed since PHP 4, released in 2000.
json_encode() wasn't added to PHP's core until PHP 5.2, in 2006,
and JSON itself didn't become the de facto standard for structured data on the web until years after that.
WordPress's earliest database schema decisions - including how options and metadata get stored - were made in
a world where serialize was simply the obvious, built-in way to stuff a PHP array into a single database
column. There was no established alternative to reach for instead.
Even setting history aside, the two formats aren't functionally identical, which is part of why serialize
never got fully displaced even after JSON became ubiquitous. PHP's serialize format preserves distinctions that
JSON structurally cannot: it knows the difference between the integer 1
and the string "1", where JSON has no separate wire representation
for "numeric string" versus "number" once you're relying on PHP's own type juggling on the way back out. It can
also represent PHP-specific object details - private and protected property visibility, for instance - that
would need extra, hand-written logic to reconstruct faithfully through json_encode()
and json_decode(). For values that only ever need to round-trip
through PHP itself, never through a non-PHP consumer, serialize actually loses less information than JSON
does.
Why WordPress Specifically Can't Just Switch
The historical argument explains why serialize was the original choice. It doesn't explain why WordPress still
uses it decades later, when switching wp_options and postmeta to JSON storage is technically straightforward.
The real reason is backward compatibility, and it's less about WordPress core itself than about everything
built on top of it. Thousands of plugins and themes - going back to WordPress's earliest years and still
actively maintained today - read and write those columns directly, calling PHP's own
maybe_unserialize() or raw unserialize()
against whatever string is stored there, and assuming it's in the serialize format because that's what
WordPress has always given them.
Changing the underlying storage format in WordPress core would mean every one of those plugins and themes - code WordPress's maintainers don't control and, in most cases, can't even audit - would need to be updated to read the new format, or would silently start failing to read their own settings on upgrade. For a platform that powers a huge share of the web specifically because of how reliably old plugins keep working across core updates, that's not a plausible trade to make. The serialize format isn't there because anyone thinks it's the best possible choice in 2026 - it's there because the cost of migrating away from it, spread across the entire plugin ecosystem, dwarfs whatever convenience JSON would offer instead.
The Practical Pain Point: Domain Migrations and Search-Replace
This is where the length-prefixing detail from earlier stops being trivia and starts being a real operational hazard. Anyone who has moved a WordPress site to a new domain - staging to production, an old URL to a new one after a rebrand - knows the standard advice: run a search-and-replace across the database to swap every occurrence of the old domain for the new one, since WordPress hardcodes the site URL into a surprising number of places, serialized arrays among them.
A naive, direct find-and-replace against the raw database text breaks exactly the values it's trying to fix.
If a serialized string contains s:19:"http://old-site.com" and a
blunt text replacement swaps old-site.com for
new-site.com, the URL text is now correct but the length prefix
is still 19 - the original byte count - while the replacement
string is a different length. PHP's unserialize() reads that
length prefix literally: it grabs exactly the declared number of bytes as the string's content and expects a
closing quote and semicolon immediately after. With the length now wrong, that boundary check fails, and
depending on the surrounding data, the whole value - or worse, everything nested after it in the same
structure - can fail to unserialize at all. Widgets disappear, plugin settings silently reset to defaults, and
the site can end up in a state that's hard to fully diagnose because the failure happens deep inside PHP's own
parsing logic, not anywhere in your own code.
This single failure mode is the entire reason dedicated WordPress migration tools exist - plugins like
Better Search Replace or WP-CLI's search-replace command don't
do a blind text substitution against the database dump. They actually unserialize each value first, perform
the replacement against the real underlying string, then re-serialize it with a freshly recalculated length
prefix, before writing it back. The lesson generalizes past WordPress: any time you're editing data in this
format, the transformation needs to go through a real serialize/unserialize round trip rather than a text
editor's find-and-replace, precisely because the format encodes structural metadata (the length) inline with
the content it's describing.
Seeing the Round Trip Yourself
If you want to see exactly what breaks and why, it's worth trying it hands-on rather than taking the explanation on faith. Take a small serialized string, decode it with the Unserialize tool to see its real structure, then manually edit the decoded text of one value (changing a domain name, for example) and re-encode it with the Serialize tool to see the length prefix update correctly on its own. Then compare that to what happens if you instead try editing the original serialized string directly and changing only the text, leaving the old length in place - pasting that corrupted version back into Unserialize is the fastest way to see, concretely, the exact parsing failure a botched domain migration produces in a live WordPress database.
