PHP json_validate(): Stop Decoding JSON Just to Check If It's Valid
Scott Keck-Warren • October 30, 2026
I was reviewing some PHP code generated by an LLM the other day and ran into this check that validates JSON from a webhook before queuing it:
if (! json_decode($payload)) {
return response()->json(['error' => 'Invalid JSON'], 400);
}
ProcessWebhook::dispatch($payload);
return response()->json(['message' => 'webhook queued'], 200);
What jumps out to me is that we're running json_decode(), which builds a metric ton of stdClass objects just to validate if we can decode it, and then we're discarding the results. This is just wasting memory and CPU cycles, and because this endpoint is called hundreds of times an hour, it's a LOT of waste.
It also has a bug. json_decode() returns a falsy value for valid payloads like [], 0, false, null, and "", and the ! treats every one of them as a failure. A webhook that sends an empty array gets a 400.
My feedback was that PHP 8.3 added json_validate() for this so we didn't have to "waste" CPU/memory. This post covers what json_validate() does, when pairing it with json_decode() parses the same string twice, and what it won't catch.
What Does json_validate() Do?
Because this was such a common problem, PHP 8.3 added the json_validate() function, which accepts a JSON-encoded string and returns a boolean indicating if it's valid. Like most well-named functions, it does what it says it's going to do.
The full signature is:
function json_validate(string $json, int $depth = 512, int $flags = 0): bool
$depth is the maximum nesting depth, and $flags only supports JSON_INVALID_UTF8_IGNORE.
So now in PHP 8.3+ we can do:
if (! json_validate($payload)) {
return response()->json(['error' => 'Invalid JSON'], 400);
}
ProcessWebhook::dispatch($payload);
return response()->json(['message' => 'webhook queued'], 200);
With less overhead. I benchmarked both against a 440 KB payload (10,000 objects) on PHP 8.4, 100 iterations each:
$payload = json_encode(array_fill(0, 10_000, ['id' => 1, 'name' => 'Scott', 'tags' => ['a', 'b', 'c']]));
$start = hrtime(true);
for ($i = 0; $i < 100; $i++) {
json_validate($payload); // swap in json_decode($payload) for the second run
}
printf("%.1f ms, peak %.1f MB\n", (hrtime(true) - $start) / 1e6, memory_get_peak_usage() / 1048576);
| Function | Time | Peak memory |
|---|---|---|
json_decode() |
375 ms | 9.4 MB |
json_validate() |
307 ms | 1.1 MB |
The time savings are modest (but not zero) because both functions still read every byte. The memory savings are what add up.
Should You Call json_validate() Before json_decode()?
json_validate() fits when you pass the string along untouched. If the same code also needs the data, you end up parsing it twice.
I had my LLM update the logic, but it also updated the job:
// ProcessWebhook::handle()
if (! json_validate($payload)) {
// JSON may contain sensitive info, so don't log it
throw new Exception("invalid json");
}
$data = json_decode($payload, true);
$this->processData($data);
In this case, we're validating and then decoding the same string, so PHP parses it twice.
A better solution is to decode it once:
// ProcessWebhook::handle()
$data = json_decode($payload, true);
if (JSON_ERROR_NONE !== json_last_error()) {
// JSON may contain sensitive info, so don't log it
throw new Exception("invalid json");
}
$this->processData($data);
Since PHP 7.3, you can also pass JSON_THROW_ON_ERROR and drop the json_last_error() check. json_decode($payload, true, flags: JSON_THROW_ON_ERROR) throws a JsonException on bad input. json_validate() rejects that flag with a ValueError, but it does set json_last_error_msg(), so you can still log why a payload failed without logging the payload.
What Doesn't json_validate() Catch?
json_validate() checks syntax, not the structure. All of these return true:
json_validate('42'); // true
json_validate('"hi"'); // true
json_validate('null'); // true
But they're not valid for our purposes. So we should have some kind of structural validation before we process them:
// ProcessWebhook::handle()
$data = json_decode($payload, true);
if (JSON_ERROR_NONE !== json_last_error()) {
// JSON may contain sensitive info, so don't log it
throw new Exception("invalid json");
}
$this->validateDataOrThrowException($data);
$this->processData($data);
How Do You Get json_validate() Below PHP 8.3?
If you're stuck below PHP 8.3 (first of all, get a move on), the symfony/polyfill-php83 package provides json_validate() for older versions. If you look at the implementation, it's just using json_decode() underneath, so there's not a huge benefit except for readability. It also means the validate-then-decode pattern from earlier runs two full decodes.
The Rules I Gave My LLM
- If you only need to know the string is valid JSON and you pass it along untouched, use
json_validate(). - If you need the data, decode once and check for errors with
JSON_THROW_ON_ERRORorjson_last_error(). - Don't use
! json_decode()as a validity check. It rejects[],0,false,null, and"". - Valid JSON isn't valid input. Check the structure after decoding.
- Below PHP 8.3, the polyfill's
json_validate()isjson_decode()underneath.
What rules have you had to give your LLM to stop it from writing code like this?
AI notice:
The outline for this post was generated via AI, then I wrote the prose and ran it through Grammarly.