Reading and Writing JSON
json_encode and json_decode with JSON_THROW_ON_ERROR, choosing arrays over objects, and the output flags that matter.
json_encode(mixed $value, int $flags = 0, int $depth = 512): string|falseJSON (JavaScript Object Notation) is a text format for data. A browser, a mobile app or another server can read it, which is why it is how most programs hand structured data to each other. PHP converts between its own values and JSON with two functions.
Encoding and decoding
json_encode() turns a PHP value into a JSON string. json_decode() turns a JSON string back into a PHP value. Both take flags as a single integer, combined with |.
Always pass JSON_THROW_ON_ERROR, added in PHP 7.3. Without it, json_encode() returns false on failure and json_decode() returns null, and you have to remember to call json_last_error() yourself. With it, a failure throws JsonException, which an ordinary try/catch handles.
An array or an object
json_decode($json, true) gives nested arrays, read with $data['key']. Leaving the second argument out gives stdClass objects, read with $data->key. Arrays are usually easier, because isset(), ?? and every array function work on them. Pick one and stay with it across a project.
Flags worth knowing
JSON_UNESCAPED_SLASHES stops / becoming \/ inside URLs. JSON_UNESCAPED_UNICODE keeps non-ASCII characters readable: é stays é instead of being escaped as é. JSON_PRESERVE_ZERO_FRACTION keeps 10.0 from printing as 10. JSON_PRETTY_PRINT is for humans reading a log, not for a response.
Two things catch people. Encoding fails on text that is not valid UTF-8, reporting "Malformed UTF-8 characters". And an empty PHP array encodes as [], never {}, which breaks a consumer expecting an object.
Example
When to use it
- Store a flexible block of settings in one database column instead of adding a column for every new option.
- Hand a list of products from PHP to JavaScript running on the same page.
- Read the payload a payment provider posts to your webhook endpoint.
- Write a cache file that another language, or a later version of your own code, can still read.
More examples
Two flag sets for two audiences
Flag combinations are easy to get wrong when they are retyped at every call site. Naming them once as constants means a response is never accidentally pretty-printed and a URL is never escaped in one endpoint but not another. Reach for this as soon as a project has more than two or three encode calls.
<?php
declare(strict_types=1);
final class JsonPresenter
{
// For an API response: smallest body, URLs left alone.
public const RESPONSE = JSON_THROW_ON_ERROR
| JSON_UNESCAPED_SLASHES
| JSON_UNESCAPED_UNICODE;
// For a log file or a debug dump: a human has to read it.
public const READABLE = self::RESPONSE | JSON_PRETTY_PRINT;
public static function response(mixed $value): string
{
return json_encode($value, self::RESPONSE);
}
public static function readable(mixed $value): string
{
return json_encode($value, self::READABLE);
}
}
$payload = ['city' => 'Málaga', 'path' => '/orders/41'];
echo JsonPresenter::response($payload), "\n";
// {"city":"Málaga","path":"/orders/41"}
echo JsonPresenter::readable($payload), "\n";
// {
// "city": "Málaga",
// "path": "/orders/41"
// }
Deciding what your own class looks like as JSON
By default json_encode() exports an object's public properties and silently skips the private ones, so a well-encapsulated class often encodes as {}. Implementing JsonSerializable puts the decision in the class instead of forcing every caller to build an array by hand. Use it for value objects such as money, dates or identifiers.
<?php
declare(strict_types=1);
final class Money implements JsonSerializable
{
public function __construct(
private readonly int $cents,
private readonly string $currency,
) {
}
// json_encode() calls this and encodes whatever it returns.
public function jsonSerialize(): array
{
return [
'amount' => $this->cents / 100,
'currency' => $this->currency,
];
}
}
$line = ['sku' => 'TS-01', 'price' => new Money(1250, 'EUR')];
echo json_encode($line, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION), "\n";
// {"sku":"TS-01","price":{"amount":12.5,"currency":"EUR"}}
Checking a string before you trust it
Before PHP 8.3 the only way to test a string was to decode it and look at the error. json_validate() separates the two questions. The second check matters just as much: a bare string, a number and null are all valid JSON documents, so is_array() is what tells you a JSON object actually arrived.
<?php
declare(strict_types=1);
// json_validate() was added in PHP 8.3. It answers true or false without
// building the decoded value, so it is cheap on a large string.
function isUsableConfig(string $raw): bool
{
if (!json_validate($raw)) {
return false;
}
// Valid JSON is not the same as the shape you expect.
$decoded = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
return is_array($decoded) && isset($decoded['version']);
}
var_dump(isUsableConfig('{"version":2}')); // bool(true)
var_dump(isUsableConfig('{"version":')); // bool(false)
var_dump(isUsableConfig('"version"')); // bool(false) - valid JSON, wrong shape
Discussion