JSON in PHP
Encoding and decoding JSON with PHP's built-in functions — plus the flags and array-type quirks that produce surprising output.
Two Functions, Several Flags
PHP ships JSON support in the core — no dependency, no configuration. The entire API is two functions, and almost everything worth knowing is in their flags.
<?php
// PHP → JSON
$json = json_encode($data);
// JSON → PHP
$data = json_decode($json); // stdClass objects
$data = json_decode($json, true); // associative arraysThat second argument matters more than it looks. Without it you get stdClass objects and access values with ->; with true you get arrays and use []. Most PHP codebases prefer arrays, because array functions, isset() checks and foreach all work naturally. Pick one convention and apply it consistently — mixing them across a codebase guarantees a Trying to access array offset on value of type object error eventually.
Handle Errors Properly
json_decode returns null when it fails. It also returns null when the input was the valid JSON document null. Checking the return value alone therefore cannot distinguish success from failure, and a great deal of PHP code gets this wrong.
<?php
// Broken: cannot tell a parse failure from a decoded null
$data = json_decode($raw);
if ($data === null) { /* which was it? */ }
// Correct for PHP 7.3+ — failures throw
try {
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
error_log("Invalid JSON: " . $e->getMessage());
}
// Older PHP — inspect the error code explicitly
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Invalid JSON: " . json_last_error_msg());
}Use JSON_THROW_ON_ERROR on both functions in any code written for PHP 7.3 or later. Silent null returns turn a clear parse failure into a null-reference error three layers away.
The Flags That Matter
PHP's default output is valid but frequently ugly. These four flags fix the common complaints:
<?php
$data = ["url" => "https://example.com/docs", "city" => "Bengaluru"];
echo json_encode($data);
// {"url":"https:\/\/example.com\/docs","city":"Bengaluru"}
echo json_encode(
$data,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
// {
// "url": "https://example.com/docs",
// "city": "Bengaluru"
// }| Flag | Effect |
|---|---|
| JSON_PRETTY_PRINT | Indents with four spaces. Development and config files only. |
| JSON_UNESCAPED_SLASHES | Stops / becoming \/. Makes URLs readable. |
| JSON_UNESCAPED_UNICODE | Emits real characters instead of \uXXXX escapes. |
| JSON_THROW_ON_ERROR | Raises JsonException rather than returning false or null. |
| JSON_PRESERVE_ZERO_FRACTION | Keeps 1.0 as 1.0 instead of collapsing to 1. |
The Array Ambiguity
This is PHP's most distinctive JSON problem, and it has no equivalent in most other languages. PHP has a single array type that serves as both an ordered list and a string-keyed map. JSON has two separate types. So json_encode must guess which one you meant.
The rule: an array whose keys are sequential integers starting at zero encodes as a JSON array. Anything else encodes as a JSON object.
<?php
json_encode([1, 2, 3]); // [1,2,3] sequential from 0
json_encode([0 => "a", 1 => "b"]); // ["a","b"] still sequential
json_encode([1 => "a", 2 => "b"]); // {"1":"a","2":"b"} starts at 1!
json_encode(["x" => 1]); // {"x":1} string keys
// The trap: filtering leaves gaps in the keys
$users = [0 => "alice", 1 => "bob", 2 => "carol"];
$active = array_filter($users, fn($u) => $u !== "bob");
json_encode($active); // {"0":"alice","2":"carol"} ← object!
// Fix: reindex before encoding
json_encode(array_values($active)); // ["alice","carol"]That array_filter case causes real production incidents. The function preserves original keys, so removing a middle element leaves a gap, and your endpoint that has always returned a JSON array suddenly returns an object. Every client iterating it breaks. Call array_values() before encoding anything that must be a JSON array.
The reverse case: an empty array is ambiguous and defaults to []. If a field must always be an object, cast it explicitly:
<?php
json_encode(["meta" => []]); // {"meta":[]}
json_encode(["meta" => (object) []]); // {"meta":{}}Encoding Objects and Classes
By default json_encode serialises only an object's public properties, silently dropping private and protected ones. Implement JsonSerializable to take control:
<?php
class User implements JsonSerializable
{
public function __construct(
private int $id,
private string $name,
private string $passwordHash,
private DateTimeImmutable $createdAt,
) {}
public function jsonSerialize(): array
{
return [
"id" => $this->id,
"name" => $this->name,
"created_at" => $this->createdAt->format(DATE_ATOM),
// passwordHash deliberately omitted
];
}
}
echo json_encode(new User(1, "Alice", "hash", new DateTimeImmutable()));This is the right place to handle dates too. JSON has no date type, so a DateTimeImmutable left to the default encoder produces an internal object of date, timezone_type and timezone fields. Formatting with DATE_ATOM gives you an ISO 8601 string that every consumer understands.
Returning JSON From an Endpoint
<?php
header("Content-Type: application/json; charset=utf-8");
echo json_encode(
["data" => $users, "meta" => ["total" => $total]],
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
exit;Set the Content-Type header before any output. If a warning, notice or stray whitespace outside a ?> tag is emitted first, it lands at the start of the response body — and the client's parser reports "Unexpected token <" or a similar error that has nothing to do with your JSON. That mismatch is described further in the JSON errors guide.
Frequently Asked Questions
Why does json_decode return null?
Because the input was not valid JSON. json_decode returns null on failure rather than throwing, and null is also a legitimate decoded value, so the return alone cannot tell you what happened. Either check json_last_error(), or pass the JSON_THROW_ON_ERROR flag so failures raise a JsonException.
Should json_decode return an array or an object?
Passing true as the second argument gives associative arrays, which is what most PHP code wants — array syntax, array functions, and simple isset checks. The default returns stdClass objects, which read more naturally for nested access but cannot use array functions.
Why does my empty PHP array encode as [] when I wanted {}?
PHP uses one array type for both lists and maps, so json_encode has to guess. An array with sequential integer keys from zero becomes a JSON array; anything else becomes an object. An empty array is ambiguous and defaults to []. Cast to (object) or pass JSON_FORCE_OBJECT to get {}.
Why are my slashes escaped as \/ ?
PHP escapes forward slashes by default, a legacy behaviour intended to make embedding JSON in HTML script tags safer. It is valid JSON and parses identically, but it makes URLs unreadable. Pass JSON_UNESCAPED_SLASHES to turn it off.
How do I handle large JSON files in PHP?
json_decode loads everything into memory and will hit memory_limit on large files. For big datasets, use JSON Lines and read one line at a time with fgets, or install a streaming parser such as halaxa/json-machine which yields records incrementally.