PHP: json_encode / json_decode error handling.

Unfortunately, the error handling in the earlier versions of PHP is non-existent. It wasn’t until PHP version 5.3.0 that the function json_last_error was introduced that. Before that, json_encode would return a NULL value or a FALSE value and you’d have to use an external JSON validation tool to try and figure out what was going wrong.

For example, if your data had special (non-UTF8) characters, the json_encode function would often return a NULL value. If the depth of your PHP array was too “deep”, then it would return a FALSE value. The json_decode function was also pretty similar in the way that it returned NULL whenever it received malformed / incorrect JSON strings.

Prior to PHP 5.3.0, JSON error handling in PHP looked a little like this (throwing Exceptions):

$val = json_encode($myArr);
if($val === false || is_null($val)){
    throw new Exception('Could not encode JSON');
}

or, with json_decode:

$val = json_decode($structure, true);
if(!is_array($val)){
    throw new Exception('Could not decode JSON');
}

Thankfully, in PHP 5.3.0, the function json_last_error was introduced. If an error has occurred, it will return one of the following constants:

  • JSON_ERROR_NONE: No error has occured.
  • JSON_ERROR_DEPTH: Max depth exceeded.
  • JSON_ERROR_STATE_MISMATCH: Invalid JSON.
  • JSON_ERROR_CTRL_CHAR: Bad character was found.

In PHP 5.3.3, another constant was added:

  • JSON_ERROR_UTF8: Bad UTF8 character was found. Incorrect encoding.

Then, in PHP 5.5, they added three more constants:

  • JSON_ERROR_RECURSION: Recursion detected.
  • JSON_ERROR_INF_OR_NAN: One or more NAN or INF values in the value to be encoded.
  • JSON_ERROR_UNSUPPORTED_TYPE: An unsupported type was found.

Using these constants, we can carry out our error handling like so:

<?php

//Attempt to decode JSON.
$decoded = json_decode($arr);

//Backwards compatability.
if(!function_exists('json_last_error')){
    if($decoded === false || $decoded === null){
        throw new Exception('Could not decode JSON!');
    }
} else{
    
    //Get the last JSON error.
    $jsonError = json_last_error();
    
    //In some cases, this will happen.
    if(is_null($decoded) && $jsonError == JSON_ERROR_NONE){
        throw new Exception('Could not decode JSON!');
    }
    
    //If an error exists.
    if($jsonError != JSON_ERROR_NONE){
        $error = 'Could not decode JSON! ';
        
        //Use a switch statement to figure out the exact error.
        switch($jsonError){
            case JSON_ERROR_DEPTH:
                $error .= 'Maximum depth exceeded!';
            break;
            case JSON_ERROR_STATE_MISMATCH:
                $error .= 'Underflow or the modes mismatch!';
            break;
            case JSON_ERROR_CTRL_CHAR:
                $error .= 'Unexpected control character found';
            break;
            case JSON_ERROR_SYNTAX:
                $error .= 'Malformed JSON';
            break;
            case JSON_ERROR_UTF8:
                 $error .= 'Malformed UTF-8 characters found!';
            break;
            default:
                $error .= 'Unknown error!';
            break;
        }
        throw new Exception($error);
    }
}

Unfortunately, this isn’t the type of code that you should be repeating throughout your script. If you stay true to the DRY (Don’t Repeat Yourself) principle (which you should), then you will wrap this kind of error checking into a function or a class method.

Personally, I would prefer it if json_last_error returned a string-based error message (like the internal XML library does). That way, you wouldn’t need to implement an ugly-looking switch statement.

Still, at least it is far more informative than it used to be!