JSON, XML and Serialization
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05. JSON is the format of APIs, configuration and JavaScript front ends; XML still dominates feeds, office documents, SOAP and publishing (ONIX for books, MARCXML for library catalogues); PHP’s native serialisation is for PHP-to-PHP caching only.
JSON
json_encode() turns a value into a JSON string; json_decode() parses one. Arrays with keys 0..n-1 become JSON
arrays, other arrays and objects become JSON objects; public properties of objects are encoded unless the class
implements JsonSerializable.
<?php
declare(strict_types=1);
$book = ['title' => 'Łódź Stories', 'year' => 1979, 'tags' => [], 'price' => 10.0, 'url' => 'https://example.org/b/1'];
echo json_encode($book), "\n";
// {"title":"Łódź Stories","year":1979,"tags":[],"price":10,"url":"https:\/\/example.org\/b\/1"}
echo json_encode($book, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
| JSON_PRESERVE_ZERO_FRACTION | JSON_PRETTY_PRINT), "\n";
echo json_encode(['tags' => new stdClass()]), ' ', json_encode([1 => 'a', 2 => 'b']), "\n";
// {"tags":{}} {"1":"a","2":"b"} -- an empty object needs an object; non-list keys produce an object
{
"title": "Łódź Stories",
"year": 1979,
"tags": [],
"price": 10.0,
"url": "https://example.org/b/1"
}
Useful encoding flags: JSON_THROW_ON_ERROR, JSON_PRETTY_PRINT, JSON_UNESCAPED_UNICODE, JSON_UNESCAPED_SLASHES,
JSON_PRESERVE_ZERO_FRACTION (keep 10.0), JSON_FORCE_OBJECT, JSON_INVALID_UTF8_SUBSTITUTE and, for embedding in
HTML <script> blocks, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT.
Source: json_encode and JSON constants
Decoding and errors
json_decode($json) returns stdClass objects; pass true (or JSON_OBJECT_AS_ARRAY) for associative arrays, which
are easier to validate. Historically, failure returned null — indistinguishable from a valid "null" — and had to
be checked with json_last_error(). Always pass JSON_THROW_ON_ERROR (7.3) instead, which throws JsonException.
<?php
declare(strict_types=1);
$payload = '{"isbn": "978-0-00-000001-1", "copies": 3, "id": 12345678901234567890}';
$data = json_decode($payload, true, flags: JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING);
var_dump($data['copies'], $data['id']); // int(3) string(20) "12345678901234567890"
try {
json_decode('{"isbn": "978-0",}', true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo $e->getMessage(), "\n"; // Syntax error
}
var_dump(json_validate('{"isbn": "978-0"}'), json_validate('{isbn: 1}')); // bool(true) bool(false) -- 8.3
try {
json_encode("broken \xff byte", JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo $e->getMessage(), "\n"; // Malformed UTF-8 characters, possibly incorrectly encoded
}
json_validate() (8.3) checks syntax without building the value, which saves memory when you only need to accept or
reject a large payload. Decoding never validates structure: check the types and keys you expect (or map the data to
typed objects), because the input is untrusted. Upcoming in PHP 8.6, JSON error messages include the error’s location.
Source: json_decode, JsonException and json_validate
JsonSerializable
Implement JsonSerializable to control how an object is encoded, for example to expose private state, rename keys or
format dates:
<?php
declare(strict_types=1);
final readonly class Loan implements JsonSerializable
{
public function __construct(
private string $isbn,
private DateTimeImmutable $due,
private int $renewals = 0,
) {}
public function jsonSerialize(): array
{
return [
'isbn' => $this->isbn,
'due_at' => $this->due->format(DateTimeInterface::ATOM),
'renewable' => $this->renewals < 2,
];
}
}
echo json_encode([new Loan('978-0-00-000001-1', new DateTimeImmutable('2026-10-26T00:00:00Z'))], JSON_THROW_ON_ERROR), "\n";
// [{"isbn":"978-0-00-000001-1","due_at":"2026-10-26T00:00:00+00:00","renewable":true}]
Backed enums encode as their value; see Enumerations.
Source: JsonSerializable
PHP serialisation
serialize() produces a PHP-specific string that unserialize() turns back into the same value, including objects
of the original class. It is suitable for caches and session data written and read by the same application. Never
unserialise untrusted input: use ['allowed_classes' => false] or a list of classes, and prefer JSON for anything that
crosses a trust boundary. Object customisation (__serialize()/__unserialize()) and the security details are in
Magic Methods and Overloading.
<?php
declare(strict_types=1);
$cached = serialize(['titles' => ['Kindred', 'Solaris'], 'built' => true]);
echo $cached, "\n"; // a:2:{s:6:"titles";a:2:{i:0;s:7:"Kindred";i:1;s:7:"Solaris";}s:5:"built";b:1;}
print_r(unserialize($cached, ['allowed_classes' => false]));
Source: serialize and unserialize
XML
PHP’s XML extensions are built on libxml2 (and, for HTML5, Lexbor since 8.4):
| API | Use |
|---|---|
SimpleXML |
quick reading of known, small documents with object syntax |
|
the legacy DOM: full read/write tree, XPath; its HTML parser is HTML 4 only |
|
the new, spec-compliant DOM: HTML5 parsing, |
|
streaming pull parser for very large files |
|
streaming writer |
|
XSLT 1.0 transformations |
SimpleXML and XPath
<?php
declare(strict_types=1);
$xml = <<<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<catalogue>
<book isbn="978-0-00-000001-1"><title>Kindred</title><year>1979</year></book>
<book isbn="978-0-00-000003-5"><title>Solaris</title><year>1961</year></book>
</catalogue>
XML;
$catalogue = simplexml_load_string($xml);
foreach ($catalogue->book as $book) {
printf("%s %s (%d)\n", $book['isbn'], $book->title, (int) $book->year); // attributes by [ ], elements by ->
}
$old = $catalogue->xpath('//book[year < 1970]/title');
echo 'before 1970: ', (string) $old[0], "\n"; // before 1970: Solaris
// Parse errors: collect them instead of warnings
libxml_use_internal_errors(true);
var_dump(simplexml_load_string('<catalogue><book>')); // bool(false)
echo count(libxml_get_errors()) > 0 ? "errors collected\n" : '';
libxml_clear_errors();
SimpleXML values are SimpleXMLElement objects: cast them with (string) or (int). Since PHP 8.0 libxml’s external
entity loading is disabled by default, which protects against XXE (XML External Entity) attacks; do not re-enable it
with LIBXML_NOENT on untrusted input.
Source: SimpleXML and SimpleXMLElement::xpath
The new DOM API: Dom\HTMLDocument and Dom\XMLDocument (8.4)
PHP 8.4 added a new, standards-compliant DOM in the Dom namespace. Dom\HTMLDocument parses HTML5 correctly
(the legacy DOMDocument::loadHTML() only understands HTML 4 and mangles modern markup); both new document classes
support CSS selectors through querySelector()/querySelectorAll(), innerHTML, classList, and since PHP 8.5
outerHTML. The legacy classes remain for compatibility.
<?php
declare(strict_types=1);
$html = <<<'HTML'
<!doctype html>
<main>
<article class="review" data-stars="5"><h2>Kindred</h2><p>Gripping & humane.</p></article>
<article class="review" data-stars="3"><h2>Solaris</h2><p>Strange.</p></article>
</main>
HTML;
$doc = Dom\HTMLDocument::createFromString($html);
foreach ($doc->querySelectorAll('article.review') as $review) {
printf("%s: %d stars\n", $review->querySelector('h2')->textContent, (int) $review->getAttribute('data-stars'));
}
echo $doc->querySelector('article p')->outerHTML, "\n"; // <p>Gripping & humane.</p> (outerHTML: 8.5)
$xml = Dom\XMLDocument::createFromString('<catalogue><book isbn="978-0-00-000001-1"/></catalogue>');
echo $xml->querySelector('book')->getAttribute('isbn'), "\n";
Kindred: 5 stars
Solaris: 3 stars
<p>Gripping & humane.</p>
978-0-00-000001-1
Streaming: XMLReader and XMLWriter
For files too large to load (a full catalogue export), XMLReader moves through the document node by node, and
XMLWriter writes without building a tree:
<?php
declare(strict_types=1);
$writer = new XMLWriter();
$writer->openMemory(); // or openUri('catalogue.xml')
$writer->setIndent(true);
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('catalogue');
foreach (['978-0-00-000001-1' => 'Kindred & Dawn', '978-0-00-000003-5' => 'Solaris'] as $isbn => $title) {
$writer->startElement('book');
$writer->writeAttribute('isbn', $isbn);
$writer->writeElement('title', $title); // escaping is automatic
$writer->endElement();
}
$writer->endElement();
$xml = $writer->outputMemory();
echo $xml;
$reader = XMLReader::fromString($xml); // 8.4 factory; XMLReader::fromUri() for files
$titles = [];
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'title') {
$titles[] = $reader->readString();
}
}
echo implode(' | ', $titles), "\n"; // Kindred & Dawn | Solaris
XSL transformations
XSLTProcessor (the xsl extension, often not installed by default) applies XSLT 1.0 stylesheets to DOM documents:
<?php
declare(strict_types=1);
$xml = new DOMDocument();
$xml->loadXML('<catalogue><book><title>Kindred</title></book><book><title>Solaris</title></book></catalogue>');
$xsl = new DOMDocument();
$xsl->loadXML(<<<'XSL'
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:for-each select="catalogue/book"><xsl:value-of select="title"/>;</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
XSL);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
echo $processor->transformToXml($xml), "\n"; // Kindred;Solaris;
Source: XSLTProcessor
Where older books and tutorials are outdated
-
Checking
json_last_error()after every call (Beak ch. 8) is superseded byJSON_THROW_ON_ERROR(7.3), andjson_validate()(8.3) is new. -
DOMDocument::loadHTML()for HTML5 produces wrong trees; useDom\HTMLDocument(8.4). -
SOAP and XML-RPC: the xmlrpc extension was removed in 8.0 (moved to PECL); REST/JSON APIs dominate, and the
soapextension remains for legacy integrations. -
XXE: libxml external entity loading is disabled by default since 8.0 and
libxml_disable_entity_loader()is deprecated. -
unserialize()withoutallowed_classeson external data is unsafe.