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.

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.

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.

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

DOMDocument, DOMXPath

the legacy DOM: full read/write tree, XPath; its HTML parser is HTML 4 only

Dom\HTMLDocument, Dom\XMLDocument (8.4)

the new, spec-compliant DOM: HTML5 parsing, querySelector(), proper namespaces

XMLReader

streaming pull parser for very large files

XMLWriter

streaming writer

XSLTProcessor (ext-xsl)

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.

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 &amp; 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 &amp; 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 &amp; 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

Source: XMLReader and XMLWriter

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 by JSON_THROW_ON_ERROR (7.3), and json_validate() (8.3) is new.

  • DOMDocument::loadHTML() for HTML5 produces wrong trees; use Dom\HTMLDocument (8.4).

  • SOAP and XML-RPC: the xmlrpc extension was removed in 8.0 (moved to PECL); REST/JSON APIs dominate, and the soap extension remains for legacy integrations.

  • XXE: libxml external entity loading is disabled by default since 8.0 and libxml_disable_entity_loader() is deprecated.

  • unserialize() without allowed_classes on external data is unsafe.