AJAX with JSON

JA JASWANT Updated 19 Oct 2026
6 min read ·Lesson 115 of 143
In this post

JSON as the Language of AJAX In simple terms: JSON is the standard way for JavaScript and PHP to exchange data. The browser sends JSON describing what it wants ("create a task called Buy notebooks "),

JSON as the Language of AJAX

In simple terms: JSON is the standard way for JavaScript and PHP to exchange data. The browser sends JSON describing what it wants ("create a task called Buy notebooks"), and PHP answers with JSON describing the result. For this conversation to work smoothly, both sides should follow consistent rules: the same structure for successful responses, the same structure for errors, and meaningful HTTP status codes.

Technically: A JSON API endpoint reads JSON request bodies from php://input, validates them, performs the action, and responds with Content-Type: application/json, an appropriate status code, and a predictable response envelope. On the client, a small wrapper around fetch() sends JSON, parses responses, and turns error responses into exceptions with useful information. This lesson builds a small task list API with create, read, update, and delete operations.


Designing Consistent Responses

SituationStatusBody
Data returned200 OK{"data": ...}
Resource created201 Created{"data": {...new resource...}}
Deleted, nothing to return204 No Content(empty)
Malformed JSON400 Bad Request{"error": {"code": "invalid_json", "message": "..."}}
Validation failed422 Unprocessable Content{"error": {"code": "validation_failed", "message": "...", "fields": {...}}}
Not found404 Not Found{"error": {"code": "not_found", "message": "..."}}
Wrong HTTP method405 Method Not Allowed{"error": {"code": "method_not_allowed", ...}}

Two rules make client code simple:

  1. Success responses always contain data; error responses always contain error with a machine-readable code and a human-readable message.
  2. The status code always matches the outcome — the client never needs to inspect the body to know whether a request succeeded.

Validating JSON Input

<?php
// src/task-validation.php
function validateTaskInput(mixed $input): array
{
    if (!is_array($input) || array_is_list($input) && $input !== []) {
        return [null, ['_body' => 'Request body must be a JSON object.']];
    }

    $title = $input['title'] ?? '';
    $title = is_string($title) ? trim(preg_replace('/\s+/u', ' ', $title)) : '';

    if ($title === '') {
        return [null, ['title' => 'Title is required.']];
    }

    if (mb_strlen($title) > 120) {
        return [null, ['title' => 'Title must be at most 120 characters.']];
    }

    return [['title' => $title], []];
}

$cases = [
    ['title' => '  Buy   notebooks  '],
    ['title' => ''],
    ['title' => str_repeat('x', 121)],
    'not an object',
];

foreach ($cases as $case) {
    [$data, $errors] = validateTaskInput($case);
    echo json_encode($errors === [] ? ['data' => $data] : ['errors' => $errors]) . "\n";
}

Output

{"data":{"title":"Buy notebooks"}}
{"errors":{"title":"Title is required."}}
{"errors":{"title":"Title must be at most 120 characters."}}
{"errors":{"_body":"Request body must be a JSON object."}}
  • A decoded JSON body can be anything: an object, a list, a string, a number. The function first checks that it received an object (an associative array).
  • Whitespace is normalized, and the title is validated for presence and length.
  • The function returns data and errors separately, so it can be tested without HTTP.

The Endpoint: api/tasks.php

The tasks are kept in the session to keep the example self-contained; a real application would use a database table with the same API.

<?php
declare(strict_types=1);

require __DIR__ . '/../src/task-validation.php';

session_start();
$_SESSION['tasks'] ??= [];
$_SESSION['next_task_id'] ??= 1;

function respond(int $status, ?array $body = null): never
{
    http_response_code($status);
    if ($body !== null) {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    }
    exit;
}

function fail(int $status, string $code, string $message, array $fields = []): never
{
    $error = ['code' => $code, 'message' => $message];
    if ($fields !== []) {
        $error['fields'] = $fields;
    }
    respond($status, ['error' => $error]);
}

function jsonBody(): mixed
{
    try {
        return json_decode(file_get_contents('php://input', length: 10_000), true, 8, JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        fail(400, 'invalid_json', 'The request body is not valid JSON.');
    }
}

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

if ($method !== 'GET') {
    $sent = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
    if (empty($_SESSION['csrf_token']) || !is_string($sent) || !hash_equals($_SESSION['csrf_token'], $sent)) {
        fail(419, 'csrf_failed', 'Your session has expired. Please reload the page.');
    }
}

if ($id !== null && $id !== false && !isset($_SESSION['tasks'][$id])) {
    fail(404, 'not_found', 'Task not found.');
}

switch ($method) {
    case 'GET':
        respond(200, ['data' => array_values($_SESSION['tasks'])]);

    case 'POST':
        [$data, $errors] = validateTaskInput(jsonBody());
        if ($errors !== []) {
            fail(422, 'validation_failed', 'Please correct the highlighted fields.', $errors);
        }
        $task = ['id' => $_SESSION['next_task_id']++, 'title' => $data['title'], 'done' => false];
        $_SESSION['tasks'][$task['id']] = $task;
        respond(201, ['data' => $task]);

    case 'PATCH':
        $input = jsonBody();
        if (!$id || !is_array($input) || !is_bool($input['done'] ?? null)) {
            fail(422, 'validation_failed', 'Send {"done": true|false} for an existing task id.');
        }
        $_SESSION['tasks'][$id]['done'] = $input['done'];
        respond(200, ['data' => $_SESSION['tasks'][$id]]);

    case 'DELETE':
        if (!$id) {
            fail(422, 'validation_failed', 'A task id is required.');
        }
        unset($_SESSION['tasks'][$id]);
        respond(204);

    default:
        header('Allow: GET, POST, PATCH, DELETE');
        fail(405, 'method_not_allowed', 'This method is not supported.');
}

Example Exchange

RequestStatusResponse
POST api/tasks.php with {"title": "Buy notebooks"}201{"data":{"id":1,"title":"Buy notebooks","done":false}}
POST api/tasks.php with {"title": ""}422{"error":{"code":"validation_failed","message":"Please correct the highlighted fields.","fields":{"title":"Title is required."}}}
PATCH api/tasks.php?id=1 with {"done": true}200{"data":{"id":1,"title":"Buy notebooks","done":true}}
GET api/tasks.php200{"data":[{"id":1,"title":"Buy notebooks","done":true}]}
DELETE api/tasks.php?id=1204(empty)
PATCH api/tasks.php?id=99 with {"done": true}404{"error":{"code":"not_found","message":"Task not found."}}

How It Works

  1. The HTTP method chooses the operation: GET reads, POST creates, PATCH updates, DELETE removes.
  2. All changing methods require the CSRF token header.
  3. JSON bodies are size-limited, depth-limited, and decoded with exceptions; invalid JSON yields a 400 with code invalid_json.
  4. respond() and fail() guarantee the consistent envelope. Because both return never, no break is needed after them in the switch.
  5. JSON types are preserved end to end: id is a number and done a boolean, because PHP's int and bool encode as JSON numbers and booleans.

The Client Wrapper

class ApiError extends Error {
    constructor(status, error) {
        super(error?.message ?? `Request failed with status ${status}`);
        this.status = status;
        this.code = error?.code ?? 'unknown';
        this.fields = error?.fields ?? {};
    }
}

const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

async function api(method, url, body) {
    const response = await fetch(url, {
        method,
        headers: {
            Accept: 'application/json',
            ...(body !== undefined && { 'Content-Type': 'application/json' }),
            ...(method !== 'GET' && { 'X-CSRF-Token': csrfToken }),
        },
        body: body !== undefined ? JSON.stringify(body) : undefined,
    });

    if (response.status === 204) {
        return null;
    }

    let payload;
    try {
        payload = await response.json();
    } catch {
        throw new ApiError(response.status, { code: 'invalid_response', message: 'The server sent an unexpected response.' });
    }

    if (!response.ok) {
        throw new ApiError(response.status, payload.error);
    }

    return payload.data;
}

// Usage
try {
    const task = await api('POST', 'api/tasks.php', { title: titleInput.value });
    addTaskToList(task);
} catch (error) {
    if (error.code === 'validation_failed') {
        showFieldErrors(error.fields);
    } else {
        showMessage(error.message);
    }
}

Why a Wrapper?

  • Every request sends the right headers, and every response is handled the same way.
  • Errors become exceptions with status, code, message, and fields, so UI code can react precisely (show field errors for validation_failed, ask to reload for csrf_failed).
  • Unexpected non-JSON responses (such as a PHP fatal error page) are detected instead of causing confusing parse errors.

Common Mistakes

Inconsistent Response Shapes

Effect: Every client call needs special-case handling.

Fix: Use one envelope for all endpoints.

Returning Errors With Status 200

Effect: Clients treat failures as success.

Fix: Match status codes to outcomes.

Not Validating the Type of Decoded JSON

Effect: Warnings or TypeErrors when the body is a list, string, or number.

Fix: Check that the decoded value has the expected structure.

Losing Types

Effect: Numbers and booleans arrive as strings ("1", "true").

Fix: Keep values typed in PHP and avoid casting everything to strings before encoding.

Unlimited JSON Bodies

Effect: Memory and CPU exhaustion from huge or deeply nested payloads.

Fix: Limit body size and decoding depth.


Best Practices

  • Define a consistent JSON envelope for data and errors, with machine-readable error codes.
  • Use HTTP methods and status codes according to their meaning.
  • Validate the structure and types of every JSON request body.
  • Wrap fetch() in a client helper that handles headers, CSRF tokens, and errors uniformly.
  • Keep validation and business logic in testable PHP functions or classes.

Official reference: https://www.php.net/manual/en/function.json-encode.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning
Ask about this post
AI Ask about this post

Ask questions about AJAX with JSON and get answers drawn from it.

Signed-in readers only.