json_decode() returns null

json_decode does not throw by default. It returns null and expects you to ask what happened. null is also the correct result for the input null, so the return value on its own genuinely cannot tell you whether anything went wrong — json_last_error() is the only way to know.

How the message appears

PHP does not throw by default, so these strings come from json_last_error_msg() rather than from an exception — or from JsonException if you passed JSON_THROW_ON_ERROR.

  • Syntax error
  • Malformed UTF-8 characters, possibly incorrectly encoded
  • Control character error, possibly incorrectly encoded
  • Maximum stack depth exceeded

What causes it

A genuine syntax error

json_last_error_msg() returns Syntax error, which is all PHP will tell you — there is no position, no line and no column. Paste the document into the validator above to find out where it actually is.

The input was not UTF-8

JSON_ERROR_UTF8, reported as "Malformed UTF-8 characters". Latin-1 text from an older database or a file read with the wrong encoding. Convert first: mb_convert_encoding($s, "UTF-8", "ISO-8859-1").

Nesting deeper than the limit

JSON_ERROR_DEPTH. The third argument defaults to 512 levels. Deeply nested documents need it raised explicitly.

It really was null

The document contained the literal null, decoding succeeded, and the result is correctly null. if ($data === null) treats this valid document as a failure, which is a bug that only shows up on the one input nobody tests.

The fix

Pass the flag and let it throw: json_decode($s, true, 512, JSON_THROW_ON_ERROR) raises JsonException on PHP 7.3 and later, which is almost always what you wanted. Without it, check json_last_error() !== JSON_ERROR_NONE and print json_last_error_msg().

Fails to parse
{
  'name': 'Ada',
  'active': true,
}
Parses
{
  "name": "Ada",
  "active": true
}

The example travels in the URL fragment, which browsers never send to a server.

Worth knowing

  • The second argument matters: json_decode($s, true) gives associative arrays, and without it you get stdClass objects. It changes nothing about whether the document is valid, only the shape you get back.
  • Large integers lose precision by default because they become floats. JSON_BIGINT_AS_STRING keeps them intact — relevant for IDs from APIs that use 64-bit numbers.
  • The encoding direction has the same silence: json_encode returns false rather than throwing, and also needs JSON_THROW_ON_ERROR to be useful.

Your JSON and text are processed entirely in your browser and are never sent anywhere. To understand how the site is used we record each page view — the page, the referrer and an approximate location from your IP address — and keep it for 90 days. Nothing is stored on your device unless you choose it. Accept to also store a device identifier here and record your browser and device characteristics, which is what lets a return visit be told from a new one. Decline and nothing is sent at all. We respect Do Not Track. See our privacy policy.