http clients and apis
JSON Errors And Exceptions
JSON errors happen when JSON text is malformed, too deeply nested, invalid UTF-8, or otherwise impossible for PHP to encode or decode.
In API work, JSON failures should not be silent. Invalid request JSON should usually become a 400 Bad Request. Server-side encoding failure is usually an application bug or data problem that should be logged and handled as a server error.
Decode failures
<?php
declare(strict_types=1);
$json = '{"id":123,';
try {
json_decode($json, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
echo 'Invalid JSON: ' . $exception->getMessage() . PHP_EOL;
}
// Prints:
// Invalid JSON: Syntax error
JSON_THROW_ON_ERROR turns JSON errors into JsonException. Without it, json_decode() can return null, which is ambiguous because JSON null is also valid.
Turn invalid request JSON into a clear error
<?php
declare(strict_types=1);
/**
* @return array<string, mixed>
*/
function decodeJsonRequest(string $body): array
{
try {
$decoded = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw new InvalidArgumentException('Request body must be valid JSON.', previous: $exception);
}
if (!is_array($decoded)) {
throw new InvalidArgumentException('Request body must be a JSON object.');
}
return $decoded;
}
try {
decodeJsonRequest('{"broken":');
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// Request body must be valid JSON.
The public error message is short and safe. The previous JsonException can still be logged by internal error handling if needed.
Encode failures are different
Encoding failure usually means your application tried to return data that cannot be represented as JSON. Invalid UTF-8 from legacy data is a common example.
<?php
declare(strict_types=1);
try {
json_encode(["bad" => "\xB1\x31"], JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
echo 'Could not encode response JSON.' . PHP_EOL;
}
// Prints:
// Could not encode response JSON.
For response encoding failures, do not return the raw exception message to API clients. Log enough context internally, then return a safe server error response.
Avoid json_last_error() in new code
Older PHP code often calls json_last_error() or json_last_error_msg(). You will still see this in real codebases. For new code, exceptions are usually cleaner because they keep the error next to the failed operation.
<?php
declare(strict_types=1);
$decoded = json_decode('{"id":123}', true);
if (json_last_error() === JSON_ERROR_NONE) {
echo 'Decoded using legacy error check.' . PHP_EOL;
}
// Prints:
// Decoded using legacy error check.
When maintaining older code, avoid mixing exception-style and json_last_error() style in the same helper unless there is a clear migration reason.
Log safely
When JSON parsing fails, logs can help. But request bodies may contain passwords, tokens, personal data, or payment details.
Log metadata first: endpoint, request ID, content length, user ID if safe, and the JSON error message. Be careful about logging full payloads.
What to check in a project
Check that JSON decoding uses JSON_THROW_ON_ERROR or a deliberate legacy error check.
Check that invalid request JSON returns a controlled 400-style error.
Check that response encoding failures are logged as server-side problems.
Check that decoded data is still shape-validated after successful decoding.
Check logs for accidental sensitive payload dumps.
What you should be able to do
After this lesson, you should be able to catch JsonException, turn invalid request bodies into safe client errors, treat encoding failures as server-side problems, and avoid silent JSON failure paths.
Practice
Task: Handle JSON Failures
Write a small PHP script that decodes JSON and converts failures into a safe application error.
Requirements
- Use
declare(strict_types=1);. - Decode with
JSON_THROW_ON_ERROR. - Return decoded data for a valid JSON object.
- Throw or return a safe message for invalid JSON.
- Reject valid JSON that is not an object, such as
nullor a string. - Print one valid case and two failure cases.
Check Your Work
Run the script and confirm syntax errors and wrong top-level shapes are handled differently from successful decoding.
Show solution
This solution turns low-level JSON exceptions into safe messages the API layer could return as a 400 response.
<?php
declare(strict_types=1);
/**
* @return array<string, mixed>
*/
function decodeJsonObject(string $body): array
{
try {
$decoded = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw new InvalidArgumentException('Request body must be valid JSON.', previous: $exception);
}
if (!is_array($decoded)) {
throw new InvalidArgumentException('Request body must be a JSON object.');
}
return $decoded;
}
foreach (['{"id":123}', '{"id":', 'null'] as $body) {
try {
$decoded = decodeJsonObject($body);
echo 'OK: ' . json_encode($decoded, JSON_THROW_ON_ERROR) . PHP_EOL;
} catch (InvalidArgumentException $exception) {
echo 'Error: ' . $exception->getMessage() . PHP_EOL;
}
}
// Prints:
// OK: {"id":123}
// Error: Request body must be valid JSON.
// Error: Request body must be a JSON object.
Why This Works
The valid case proves decoding works. The broken JSON case proves syntax errors are caught. The null case proves valid JSON can still be rejected when the API requires an object.