JSON Encoding & Decoding
JSON has become the default data interchange format for web APIs, configuration files, and communication between services written in different languages. PHP's built-in json_encode() and json_decode() functions convert between native PHP values and JSON text, and understanding their handful of quirky defaults saves a lot of debugging time later.
json_encode(): PHP values to JSON text
json_encode() walks a PHP value and produces the equivalent JSON string. Arrays are the interesting case: a PHP array with sequential integer keys starting at 0 becomes a JSON array, while any array with string keys (or non-sequential integer keys) becomes a JSON object.
Encoding arrays: sequential vs associative
<?php $list = ['red', 'green', 'blue']; $map = ['name' => 'Firoz', 'role' => 'developer']; echo json_encode($list), "\n"; echo json_encode($map), "\n";
["red","green","blue"]
{"name":"Firoz","role":"developer"}json_decode(): JSON text back to PHP values
By default, json_decode() turns a JSON object into a stdClass
instance, accessed with -> like any object. Passing true as the
second argument switches JSON objects to associative arrays
instead — often the more convenient shape to work with in PHP,
especially when the structure needs to be modified afterward.
stdClass vs associative array decoding
<?php
$json = '{"name": "Firoz", "role": "developer"}';
$asObject = json_decode($json);
echo $asObject->name, "\n";
echo get_class($asObject), "\n";
$asArray = json_decode($json, true);
echo $asArray['name'], "\n";
var_dump(is_array($asArray));Firoz stdClass Firoz bool(true)
Useful flags
Both functions accept a bitmask of flags as an additional argument that changes their behavior in ways that come up constantly in real code.
Flag | Effect |
|---|---|
JSON_PRETTY_PRINT | Adds indentation and line breaks to json_encode() output for human readability |
JSON_UNESCAPED_UNICODE | Outputs multibyte characters directly instead of as \uXXXX escape sequences |
JSON_UNESCAPED_SLASHES | Stops json_encode() from escaping forward slashes as / |
JSON_THROW_ON_ERROR | Makes both functions throw a JsonException on failure instead of failing silently |
Pretty-printing and preserving Unicode
<?php $data = ['city' => 'Zürich', 'nested' => ['a' => 1, 'b' => 2]]; echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
{
"city": "Zürich",
"nested": {
"a": 1,
"b": 2
}
}Without JSON_UNESCAPED_UNICODE, that same "ü" would be encoded as the escape sequence \\u00fc — technically valid JSON, but harder to read in logs and debugging output.
Checking for errors: json_last_error() vs JSON_THROW_ON_ERROR
By default, both json_encode() and json_decode() fail quietly: json_decode() returns null for invalid JSON, and json_encode() returns false for a value it can't represent (like a resource, or an array containing one). Since null is also a perfectly valid successfully-decoded value (decoding the literal text "null" correctly produces PHP null), checking the return value alone can't distinguish "decoding failed" from "the JSON really was null." The traditional fix is calling json_last_error() immediately afterward.
The traditional error-checking approach
<?php
$json = '{"broken": "json",}'; // trailing comma is invalid JSON
$result = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo 'Decode failed: ', json_last_error_msg(), "\n";
}Decode failed: Syntax error
The modern alternative is passing the JSON_THROW_ON_ERROR flag, which turns a failure into a thrown JsonException instead, letting ordinary try/catch handle it the same way as any other error condition in the codebase.
The modern throw-on-error approach
<?php
try {
$result = json_decode('{"broken": "json",}', true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo 'Decode failed: ', $e->getMessage(), "\n";
}Decode failed: Syntax error
Encoding objects: public properties, or JsonSerializable
When json_encode() is given an ordinary object, it only includes public properties — private and protected properties are skipped entirely, since JSON has no notion of visibility and json_encode() treats public state as "the data that's meant to be exposed."
Only public properties are encoded
<?php
class User
{
public string $name;
private string $passwordHash;
public function __construct(string $name, string $passwordHash)
{
$this->name = $name;
$this->passwordHash = $passwordHash;
}
}
$user = new User('Firoz', 'a1b2c3...');
echo json_encode($user), "\n";{"name":"Firoz"}When more control is needed than "just the public properties" — for example, renaming keys, formatting a nested DateTimeImmutable, or deliberately including some private state — a class can implement the JsonSerializable interface and define jsonSerialize(), which json_encode() calls instead of using its default reflection-based behavior.
Custom encoding with JsonSerializable
<?php
class User implements JsonSerializable
{
public function __construct(
private string $name,
private string $passwordHash,
private DateTimeImmutable $joinedAt,
) {
}
public function jsonSerialize(): array
{
return [
'name' => $this->name,
'joined_at' => $this->joinedAt->format('Y-m-d'),
// passwordHash is intentionally omitted
];
}
}
$user = new User('Firoz', 'a1b2c3...', new DateTimeImmutable('2024-01-10'));
echo json_encode($user), "\n";{"name":"Firoz","joined_at":"2024-01-10"}PHP arrays with sequential integer keys encode as JSON arrays; anything else encodes as a JSON object.
By default,
json_decode()producesstdClassobjects — passtrueas the second argument for associative arrays instead.JSON_PRETTY_PRINT,JSON_UNESCAPED_UNICODE, andJSON_UNESCAPED_SLASHEScontrol formatting of encoded output.JSON_THROW_ON_ERRORconverts silent failures into a catchableJsonException.Only
publicproperties are encoded by default; implementJsonSerializablefor full control over the output shape.