Lesson 84 of 100
JSON15 min5 checksPHP 8.5

Control Object JSON Serialization with JsonSerializable

Use JsonSerializable to decide exactly which fields an object exposes to json_encode(), including renamed, computed, and conditional fields.

01 · THE IDEAREAD → TRY → LEARN

Why control the JSON shape?

When you pass an object to json_encode(), PHP does not know which parts of it are meant for the outside world. Without help, it exposes only the public properties. Private and protected state is skipped, so a class with only private properties encodes as an empty object. That default is easy to break by accident: add a public property later and it leaks into your output.

The JsonSerializable interface fixes this. A class that implements it declares one method:

public function jsonSerialize(): mixed

json_encode() calls this method and encodes whatever it returns. The JSON format becomes an explicit contract, separate from how the object stores its data.

A first example

The class below keeps a price in integer cents but publishes it as a decimal string. It also chooses the key names. Integer arithmetic avoids floating-point surprises.

Example
<?php
final class Price implements JsonSerializable
{
    public function __construct(private string $sku, private int $cents) {}

    public function jsonSerialize(): array
    {
        return [
            'sku' => $this->sku,
            'price' => sprintf('%d.%02d', intdiv($this->cents, 100), $this->cents % 100),
        ];
    }
}

echo json_encode(new Price('A-1', 1999), JSON_UNESCAPED_SLASHES), "\n";
Output
{"sku":"A-1","price":"19.99"}

The return value is an ordinary array, so you can reshape, rename, or compute anything. An associative array becomes a JSON object and a list becomes a JSON array.

The return value is encoded again

jsonSerialize() may return any value, such as a scalar, an array, or even other objects. If it returns objects that also implement JsonSerializable, they are serialized in turn. Nesting therefore works naturally:

Example
<?php
final class Tag implements JsonSerializable
{
    public function __construct(private string $label) {}

    public function jsonSerialize(): string
    {
        return strtolower($this->label);
    }
}

final class Post implements JsonSerializable
{
    public function __construct(private string $title, private array $tags) {}

    public function jsonSerialize(): array
    {
        return ['title' => $this->title, 'tags' => $this->tags];
    }
}

$post = new Post('Intro', [new Tag('PHP'), new Tag('JSON')]);
echo json_encode($post), "\n";
Output
{"title":"Intro","tags":["php","json"]}

Each Tag becomes a plain string, so the tags list is a JSON array of strings. The narrower return types string and array are valid because both are subtypes of mixed.

Default behavior versus an explicit contract

Compare a class with no interface to one that implements it:

Example
<?php
final class Plain
{
    public function __construct(private string $secret, private string $id) {}
}

final class Explicit implements JsonSerializable
{
    public function __construct(private string $secret, private string $id) {}

    public function jsonSerialize(): array
    {
        return ['id' => $this->id];
    }
}

echo json_encode(new Plain('s3cret', '7')), "\n";
echo json_encode(new Explicit('s3cret', '7')), "\n";
Output
{}
{"id":"7"}

The first object produces {} because nothing is public. The second produces exactly the fields you chose, and secret stays out because the method never returns it.

Design guidelines

  • Return only data that is safe to publish. Treat the method as an allow-list.
  • Keep the method free of side effects. It should just describe the current state.
  • Use array_values() when you need a list. An array with gaps in its keys would otherwise encode as an object.
  • Build conditional output by assembling the array first and adding optional keys with an if.
  • Key order in the returned array is the key order in the JSON text, which helps tests stay stable.

Exercise contract

Write user_json(string $name, string $email, array $roles, bool $includeEmail): string. Inside it, create an object whose class implements JsonSerializable, then return json_encode() of that object with JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES.

The object must serialize to an object with name first and roles second. Roles must be a re-indexed list. Add an email key last, and only when $includeEmail is true. Because the email is a private property, it must never appear unless your method adds it.

Boundary cases to consider: an empty role list must produce [], an empty name must stay an empty string, and non-ASCII text must not be escaped. Your tests compare the returned string, not printed output.

Recap

JsonSerializable turns JSON output into a deliberate API. Implement jsonSerialize(), return the exact shape you want, and let json_encode() handle nested values for you.

Before you start

Functions · Arrays · Types

You’ll practice
  • Implement JsonSerializable for an explicit data shape
  • Implement and test a deterministic PHP 8.4 function
Your task

Make user_json() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.