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
<?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";
Output
{"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
<?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));
Output
{"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
<?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);
Output
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
<?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";
}
Output
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
<?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";
Output
{"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 api.php
<?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]);
HTTP
GET /api.php
POST /api.php {"name":"Pen","qty":3}
POST /api.php {"name":"Pen","qty":0}
POST /api.php not json
Responses
> 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
<?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
<?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.

Try it yourself · main.php
Edit the code, copy it into main.php, then run it with php main.php (PHP 8.5). How to install PHP
Output
[
    {
        "title": "Pay invoice",
        "due": "2026-10-01",
        "urgent": true
    },
    {
        "title": "Plan trip",
        "due": "2026-12-20",
        "urgent": false
    }
]