In PHP, which properties does json_encode() include for an object, and how does implementing JsonSerializable change the output?
answer
- visibility decides by default
- uninitialized typed properties vanish
- jsonSerialize(): mixed
- backed enums become their value
- 8.1 tentative return type notice
basics
~10 sBy default json_encode() writes an object's initialized public properties and skips protected and private ones. A class implementing JsonSerializable is encoded as whatever its jsonSerialize(): mixed method returns instead.
solid answer
~40 sFor an ordinary object, `json_encode()` writes a JSON object of its **public** properties; protected and private ones are skipped, and so are typed properties that were never initialized. That default leaks whatever happens to be public and hides everything else, and some built-ins look odd, for example a `DateTimeImmutable` encodes as `date`, `timezone_type` and `timezone` members. Implementing `JsonSerializable` hands control to your code: `json_encode()` calls `jsonSerialize(): mixed` and encodes its return value, which can be an array, a scalar, or another serializable object. Since PHP 8.1 the method has a tentative `mixed` return type, so omitting it raises a deprecation. Enums are handled natively: a backed enum case encodes as its value, while a pure enum case fails with `JSON_ERROR_NON_BACKED_ENUM`.
code
php · 28 lines<?php
declare(strict_types=1);
enum Status: string
{
case Active = 'active';
case Lapsed = 'lapsed';
}
final class Member implements JsonSerializable
{
public function __construct(
private string $name,
private Status $status,
private DateTimeImmutable $joined,
) {}
public function jsonSerialize(): array
{
return [
'name' => $this->name,
'status' => $this->status, // encodes as "active"
'joined' => $this->joined->format(DATE_ATOM),
];
}
}
echo json_encode(new Member('Ana', Status::Active, new DateTimeImmutable('2025-03-14')), JSON_THROW_ON_ERROR);go deeper
Recall that json_encode() writes public properties only, and that JsonSerializable lets a class return its own representation.
Explain the skipping of protected, private and uninitialized properties, how enums encode, and the 8.1 return-type notice on jsonSerialize().
Stop accidental field leaks by encoding through explicit jsonSerialize() methods or DTOs, and normalise dates and money in the contract format.
Decide whether domain objects serialize themselves or a separate response layer owns the wire format, weighing coupling against boilerplate.
## The default: public properties only When `json_encode()` meets an object that does not implement `JsonSerializable` and is not an enum, it encodes it as a JSON object built from the object's properties. The encoder's source makes the rules concrete: - **public** properties are written, in declaration order, with their names as keys; - **protected** and **private** properties are skipped, whatever scope calls `json_encode()`; - **typed properties that were never initialized** are skipped rather than written as `null`; - dynamic public properties, such as those on a `stdClass`, are included. So this class: ```php final class Member { public string $name = 'Ana'; protected string $email = '[email protected]'; private string $passwordHash = '...'; public ?string $nickname; } ``` encodes as `{"name":"Ana"}`: `email` and `passwordHash` are hidden by visibility, and `nickname` is uninitialized. That is convenient and fragile at the same time. Adding a public property later silently adds it to every API response, and a value object built on private properties with getters encodes as `{}`. ## Built-in objects Some built-in classes define what `json_encode()` sees. A `DateTimeImmutable` encodes with `date`, `timezone_type` and `timezone` members, which no client wants. Format dates explicitly, for example with `->format(DATE_ATOM)`, before encoding. ## Enums | Case type | Encodes as | |---|---| | backed enum (`enum Status: string`) | its backing value, e.g. `"active"` | | pure enum (`enum Suit`) | error `JSON_ERROR_NON_BACKED_ENUM` ("Non-backed enums have no default serialization") | With `JSON_THROW_ON_ERROR`, the pure-enum case throws a `JsonException`; without it, `json_encode()` returns `false`. ## JsonSerializable: you choose the representation `JsonSerializable` is a built-in interface with one method: ```php public function jsonSerialize(): mixed; ``` When the encoder meets an object implementing it, it calls the method and encodes the **return value** instead of the object's properties. The return value can be: - an associative array, which is the usual choice for an explicit, reviewed field list; - a scalar, for value objects such as `Money` or `EmailAddress`; - another object, including one that implements `JsonSerializable` itself, handled recursively. This decouples the wire format from visibility: private state stays private, renamed properties do not rename API fields, and computed fields can be added. ## The PHP 8.1 tentative return type PHP 8.1 gave many internal methods **tentative return types**. For `JsonSerializable::jsonSerialize()` it is `mixed`. A class whose method lacks a compatible return type triggers an `E_DEPRECATED` notice ("Return type of ... should either be compatible with JsonSerializable::jsonSerialize(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice"). The fix in current code is simply to declare `: mixed` or a narrower type such as `: array`. ## What JsonSerializable does not do - It has **no decode counterpart**. `json_decode()` never calls a constructor or a static factory on your class; you map the decoded array into objects yourself. - It does not affect `serialize()`, `var_export()` or `print_r()`; those have their own mechanisms. - It is not called for arrays, only for objects implementing the interface. ## Choosing an approach 1. For API responses, prefer explicit `jsonSerialize()` methods or dedicated response DTOs over relying on public properties. 2. For backed enums, let the encoder write the value. 3. For dates, money and IDs, return the string form the contract specifies. ## Object encoding at a glance | Member of the object | Default encoding | With `JsonSerializable` | |---|---|---| | public, initialized | written | only if the method returns it | | public, uninitialized typed | skipped | only if the method returns it | | protected / private | skipped | can be returned explicitly | | computed value | not possible | returned by the method | Because the method's result is encoded with the same rules, returning an array that contains other `JsonSerializable` objects, enums or nested arrays works recursively.
- What happens when json_encode() meets a pure enum case, with and without JSON_THROW_ON_ERROR?A pure enum has no backing value, so the encoder records `JSON_ERROR_NON_BACKED_ENUM`. Without the flag, `json_encode()` returns `false` and `json_last_error_msg()` says "Non-backed enums have no default serialization"; with the flag it throws a `JsonException` carrying that code.
- Does implementing JsonSerializable let json_decode() rebuild your object?No. The interface is used only by `json_encode()`. `json_decode()` returns arrays or `stdClass` objects, and rebuilding a `Member` from them is your code's job, usually a named constructor that validates each field.
saying these in an interview costs you the question
- Believing json_encode() includes private properties when called inside the class
- Expecting uninitialized typed properties to appear as null
- Assuming JsonSerializable also controls how json_decode() builds objects
- Thinking pure enum cases encode as their case name
- Leaving jsonSerialize() without a return type on PHP 8.1+