JSON and HTTP Requests
JSON is how web services talk to each other: APIs send it, JavaScript front ends expect it, and configuration files use it. PHP converts between JSON and arrays or objects with json_encode and json_decode. This lesson covers both, plus building a JSON API endpoint and calling other APIs over HTTP.
Encoding: PHP to JSON
json_encode turns arrays, objects and scalars into a JSON string. Lists become JSON arrays; associative arrays become JSON objects:
<?php $product = [ 'id' => 42, 'name' => 'Desk lamp', 'price' => 34.5, 'inStock' => true, 'tags' => ['lighting', 'office'], 'discount' => null, ]; echo json_encode($product), "\n"; echo json_encode($product, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n"; echo json_encode(['url' => 'https://example.com/a', 'name' => 'Zoë']), "\n"; echo json_encode(['url' => 'https://example.com/a', 'name' => 'Zoë'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n";
{"id":42,"name":"Desk lamp","price":34.5,"inStock":true,"tags":["lighting","office"],"discount":null}
{
"id": 42,
"name": "Desk lamp",
"price": 34.5,
"inStock": true,
"tags": [
"lighting",
"office"
],
"discount": null
}
{"url":"https:\/\/example.com\/a","name":"Zo\u00eb"}
{"url":"https://example.com/a","name":"Zoë"}Lists versus objects
An array becomes a JSON array only if its keys are exactly 0, 1, 2… in order. Anything else — including a list with a gap after array_filter — becomes a JSON object, which often surprises JavaScript code expecting an array:
<?php $numbers = [10, 25, 3, 42]; $big = array_filter($numbers, fn($n) => $n > 5); echo json_encode($big), "\n"; echo json_encode(array_values($big)), "\n"; echo json_encode([]), " ", json_encode(new stdClass()), "\n"; var_dump(array_is_list($big));
{"0":10,"1":25,"3":42}
[10,25,42]
[] {}
bool(false)Decoding: JSON to PHP
json_decode parses JSON. Pass true as the second argument to get associative arrays; otherwise JSON objects become stdClass objects:
<?php $json = '{"user":{"name":"Ana","roles":["admin","editor"]},"active":true}'; $asArray = json_decode($json, true); echo $asArray['user']['name'], " has ", count($asArray['user']['roles']), " roles\n"; $asObject = json_decode($json); echo $asObject->user->roles[0], "\n"; var_dump($asObject->active);
Ana has 2 roles admin bool(true)
Handling invalid JSON
By default, json_decode returns null for invalid input — indistinguishable from the valid JSON null. Pass JSON_THROW_ON_ERROR to get an exception with a clear message instead. To merely check validity, PHP 8.3 added json_validate:
<?php $broken = '{"name": "Ana", }'; var_dump(json_decode($broken, true)); var_dump(json_validate($broken)); try { json_decode($broken, true, flags: JSON_THROW_ON_ERROR); } catch (JsonException $e) { echo "Invalid JSON: ", $e->getMessage(), "\n"; }
NULL bool(false) Invalid JSON: Syntax error
Always use JSON_THROW_ON_ERROR (for encoding too) — silent nulls cause confusing bugs later.
Controlling how objects are encoded
json_encode includes an object's public properties. Implement JsonSerializable to choose exactly what's output — for example to hide a password hash or format a date:
<?php final class User implements JsonSerializable { public function __construct( public int $id, public string $email, private string $passwordHash, private DateTimeImmutable $joined, ) {} public function jsonSerialize(): array { return [ 'id' => $this->id, 'email' => $this->email, 'joined' => $this->joined->format('Y-m-d'), ]; } } $u = new User(7, 'ana@example.com', '$2y$10$abc...', new DateTimeImmutable('2025-11-20')); echo json_encode($u, JSON_THROW_ON_ERROR), "\n";
{"id":7,"email":"ana@example.com","joined":"2025-11-20"}A JSON API endpoint
An API endpoint reads JSON from the request body, validates it, and responds with JSON and an appropriate status code:
<?php header('Content-Type: application/json'); function respond(int $status, array $body): never { http_response_code($status); echo json_encode($body, JSON_THROW_ON_ERROR); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { respond(405, ['error' => 'Use POST']); } try { $data = json_decode(file_get_contents('php://input'), true, flags: JSON_THROW_ON_ERROR); } catch (JsonException) { respond(400, ['error' => 'Body must be valid JSON']); } $name = trim($data['name'] ?? ''); $qty = filter_var($data['qty'] ?? null, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); if ($name === '' || $qty === false) { respond(400, ['error' => 'name and a positive qty are required']); } respond(201, ['id' => 1001, 'name' => $name, 'qty' => $qty]);
GET /api.php
POST /api.php {"name":"Pen","qty":3}
POST /api.php {"name":"Pen","qty":0}
POST /api.php not json
> GET /api.php
< 405 Method Not Allowed
{"error":"Use POST"}
> POST /api.php
< 201 Created
{"id":1001,"name":"Pen","qty":3}
> POST /api.php
< 400 Bad Request
{"error":"name and a positive qty are required"}
> POST /api.php
< 400 Bad Request
{"error":"Body must be valid JSON"}php://input gives the raw request body. Returning never from respond() tells readers (and static analysers) that it always ends the script.
Calling other APIs
The quickest way to fetch a URL is file_get_contents with a stream context that sets the method, headers and timeout. For more control — custom headers, authentication, uploading files, detailed error handling — use the curl extension. (These examples need an internet connection, so their output isn't shown.)
<?php $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'header' => "Accept: application/json\r\nUser-Agent: my-app/1.0\r\n", 'timeout' => 5, 'ignore_errors' => true, // return the body even for 4xx/5xx ], ]); $body = file_get_contents('https://api.github.com/repos/php/php-src', false, $context); $status = http_get_last_response_headers()[0] ?? 'no response'; // PHP 8.4+ $repo = json_decode($body, true, flags: JSON_THROW_ON_ERROR); echo "$status: {$repo['full_name']} has {$repo['stargazers_count']} stars\n";
<?php $ch = curl_init('https://httpbin.org/post'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['name' => 'Pen', 'qty' => 3]), CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); $response = curl_exec($ch); if ($response === false) { throw new RuntimeException('Request failed: ' . curl_error($ch)); } $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); echo "HTTP $status\n";
Always set a timeout: without one, a slow API can hang your page indefinitely. In larger applications, an HTTP client library such as Guzzle or Symfony HttpClient (installed with Composer, lesson 60) adds retries, middleware and a much friendlier API.
Try it yourself
Decode a list of tasks, mark the overdue ones as urgent, and encode the result again.
[
{
"title": "Pay invoice",
"due": "2026-10-01",
"urgent": true
},
{
"title": "Plan trip",
"due": "2026-12-20",
"urgent": false
}
]