PHPJSON Encoding & Decoding

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
<?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
<?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
<?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
<?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
<?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
<?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
<?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"}
Silent decode failures are easy to miss without a check
Code that does `$data = json_decode($input, true); echo $data['key'];` without checking for failure will, on malformed input, get a `null` `$data` and then trigger a warning (or a fatal error in newer PHP versions) trying to index into it — often far away from where the actual bad JSON came in, making the root cause harder to trace. Always pass `JSON_THROW_ON_ERROR` (or check `json_last_error()`) right at the decode site, especially for JSON coming from an external API or user upload.
  • PHP arrays with sequential integer keys encode as JSON arrays; anything else encodes as a JSON object.

  • By default, json_decode() produces stdClass objects — pass true as the second argument for associative arrays instead.

  • JSON_PRETTY_PRINT, JSON_UNESCAPED_UNICODE, and JSON_UNESCAPED_SLASHES control formatting of encoded output.

  • JSON_THROW_ON_ERROR converts silent failures into a catchable JsonException.

  • Only public properties are encoded by default; implement JsonSerializable for full control over the output shape.

Note
`json_encode()`'s fourth argument controls maximum nesting depth (default 512), and `json_decode()`'s third argument controls the same for decoding — both exist mainly as a safety limit against pathologically deep or malicious input.
Tip
Pass `JSON_THROW_ON_ERROR` on every `json_encode()`/`json_decode()` call in new code, and let exceptions propagate up to wherever errors are already being handled. It removes an entire class of "forgot to check `json_last_error()`" bugs for free.