By default, failures return null or false. json_last_error_msg() holds the reason only until the next JSON call:
<?php
echo json_encode([json_decode('{"qty":2,}'), json_decode('null'), json_last_error_msg()]), "\n";
try {
json_encode("caf\xE9", JSON_THROW_ON_ERROR); // a Latin-1 byte
} catch (JsonException $e) {
printf("%s %d: %s\n", $e::class, $e->getCode(), $e->getMessage());
}Output
[null,null,"No error"] JsonException 5: Malformed UTF-8 characters, possibly incorrectly encoded
The syntax error and the valid null look identical, and the second call erased the error. JSON_THROW_ON_ERROR (PHP 7.3) throws with a JSON_ERROR_* code (5 is JSON_ERROR_UTF8) and leaves the global error state alone. Use it everywhere, and map the exception to a 400. Convert legacy text to UTF-8 first (Multibyte Strings).