0

PHP 8.3 — json_validate() function

Beginner5 min read·php-09-020

Concept

PHP 8.3 added the json_validate() function, which checks whether a string is valid JSON without decoding it into PHP values. Before PHP 8.3, the only way to validate JSON was json_decode() followed by json_last_error(), which fully parses the input, allocates PHP objects or arrays, and only then reports failure. json_validate() does the parse in C without producing any PHP-side output, making it meaningfully faster and lower-memory for cases where you only need a yes/no answer.

The function signature is json_validate(string $json, int $depth = 512, int $flags = 0): bool. It returns true if the string is valid JSON, false otherwise. The $depth parameter matches json_decode()'s nesting depth limit: if the JSON is valid but deeper than $depth, json_validate() returns false. The $flags parameter currently accepts JSON_INVALID_UTF8_IGNORE — the same flags as json_decode().

The performance advantage is most visible in API gateways, message queue consumers, and webhook receivers that need to filter malformed payloads before expensive processing. Validating a 1 MB JSON payload with json_validate() consumes roughly 1–5% of the memory that json_decode() would allocate for the equivalent PHP array. For pipelines processing thousands of messages per second, this compounds significantly.

A gotcha: json_validate() does not tell you why a string is invalid — it returns only bool. If you need diagnostics (error position, error code), you still need json_decode() + json_last_error() + json_last_error_msg(). Use json_validate() as a cheap pre-check or final gate; reach for json_decode() when you need the parsed value or error detail.

OperationDecodes to PHPMemorySpeedError detail
json_decode()YesHighBaselineYes (json_last_error)
json_validate()No~1–5% of above~10–15x fasterNo — bool only

Code Example

php
<?php
declare(strict_types=1);

// Basic usage
$payload = '{"event":"user.created","id":42,"email":"alice@example.com"}';
$garbage = '{invalid json: true,}';

var_dump(json_validate($payload)); // bool(true)
var_dump(json_validate($garbage)); // bool(false)

// Depth limiting — protects against deeply nested DoS payloads
$deep = str_repeat('{"a":', 600) . 'null' . str_repeat('}', 600);
var_dump(json_validate($deep, depth: 512)); // bool(false) — too deep
var_dump(json_validate($deep, depth: 700)); // bool(true)

// Practical: webhook receiver that rejects invalid JSON before processing
final class WebhookReceiver
{
    public function handle(string $rawBody): array
    {
        // Cheap validation first — no memory allocation
        if (!json_validate($rawBody, depth: 64)) {
            throw new \InvalidArgumentException(
                'Webhook payload is not valid JSON (max depth 64)'
            );
        }

        // Only decode after we know it's valid and safe depth
        /** @var array<string, mixed> $data */
        $data = json_decode($rawBody, associative: true, depth: 64);

        return $data;
    }
}

// Pattern: validate before expensive deserialization in a queue consumer
function processQueueMessage(string $message): void
{
    if (!json_validate($message)) {
        // Dead-letter queue the message without touching it further
        deadLetter($message, reason: 'invalid_json');
        return;
    }

    $payload = json_decode($message, associative: true);
    // ... process payload
}

function deadLetter(string $message, string $reason): void
{
    error_log(sprintf('[dead-letter] reason=%s body=%s', $reason, substr($message, 0, 200)));
}

// Benchmark comparison (illustration — not runnable here)
$json = json_encode(range(1, 10_000)); // ~50 KB array

$start = hrtime(true);
for ($i = 0; $i < 1_000; $i++) {
    json_validate($json); // no allocation
}
$validateMs = (hrtime(true) - $start) / 1e6;

$start = hrtime(true);
for ($i = 0; $i < 1_000; $i++) {
    json_decode($json); // allocates 10,000 PHP values each iteration
}
$decodeMs = (hrtime(true) - $start) / 1e6;

printf("json_validate: %.2f ms\n", $validateMs);
printf("json_decode:   %.2f ms\n", $decodeMs);

Interview Q&A

Q: Why is json_validate() faster than json_decode() followed by json_last_error()?

json_decode() builds a full PHP data structure in the heap — it allocates zvals for every key, string, integer, and nested array in the JSON. Even when the parse fails, PHP has partially allocated memory before detecting the error. json_validate() runs the same JSON grammar check in C but writes nothing to PHP's value heap — it discards every token as soon as it's confirmed valid. The result is that validation cost is proportional only to the byte length of the input, not to the depth or number of values. For a 1 MB JSON object with thousands of keys, json_validate() uses a few KB of stack; json_decode() uses megabytes of heap.


Q: What should you do when json_validate() returns false and you need to report the error to the caller?

json_validate() returns only bool — there is no error code or position information exposed. When you need diagnostics, decode with json_decode($input, null, 512, 0), check json_last_error() against the JSON_ERROR_* constants, and read json_last_error_msg() for a human-readable description. A practical two-phase approach: use json_validate() as a quick pre-filter in high-throughput paths where most inputs are valid (reject early with minimal cost), and fall back to json_decode() + json_last_error_msg() only in error-reporting paths where an invalid input needs a detailed message.


Q: Does json_validate() protect against JSON-based DoS attacks like deeply nested payloads?

Partially. The $depth parameter limits recursion — json_validate($input, depth: 32) will return false for any payload nested deeper than 32 levels, preventing the parser from recursing indefinitely. This protects against the "billion laughs"-style nesting attack. However, json_validate() does not protect against extremely large flat arrays or long strings — a 100 MB flat array [1,2,3,...,10000000] will scan every byte. For full DoS protection, impose a Content-Length limit at the HTTP layer (nginx, max_post_size in php.ini) before the body reaches PHP, then use json_validate() with a reasonable depth limit before proceeding.