The Error
You call json_decode() and get slapped with:
json_decode() expects parameter 1 to be string, null given
Or on PHP 8:
json_decode(): Argument #1 ($json) must be of type string, null given
The function got null instead of a JSON string. The crash line is obvious โ the real problem is hiding a few lines above it.
Root Causes
- An API response or file read returned
nullor empty, and you passed it straight tojson_decode()without checking. This is the most common cause. - A function you expected to return a string actually returns
nullon failure โfile_get_contents(),$request->getContent(), and DB column reads all do this silently. - The variable was never assigned โ a typo in the variable name, a missing assignment, or a conditional branch that skipped it entirely.
- JSON came from user input or an external service that sent nothing: empty POST body, missing query param, or a 4xx response with no body.
Fix 1 โ Guard Before Decoding
Check that the value is a non-empty string before you even touch json_decode(). Simple, and it forces you to decide what "failure" means for your code.
<?php
$raw = file_get_contents('https://api.example.com/data');
if ($raw === false || $raw === null || $raw === '') {
// Log it, throw an exception, return early โ pick one
throw new RuntimeException('Failed to fetch JSON data');
}
$data = json_decode($raw, true);
With user input from a request:
<?php
$body = $request->getContent(); // Could be an empty string
if (empty($body)) {
return response()->json(['error' => 'Empty request body'], 400);
}
$data = json_decode($body, true);
Fix 2 โ Check json_last_error() After Decoding
Here's a trap: json_decode() returns null for two very different situations โ the input was null, and the JSON was malformed. Without checking, you can't tell which one happened.
<?php
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('JSON decode failed: ' . json_last_error_msg());
}
This catches invalid syntax, unexpected encoding, and truncated responses โ failures that would otherwise slip through silently and corrupt your data downstream.
Fix 3 โ Null Coalescing for Optional Data
Sometimes a missing value is perfectly normal โ a nullable DB column, an optional config field. In that case, provide a safe fallback instead of bailing out:
<?php
// If $row['metadata'] is null, decode an empty object instead
$metadata = json_decode($row['metadata'] ?? '{}', true);
// Or for an array
$tags = json_decode($row['tags'] ?? '[]', true);
Pick a fallback that actually makes sense for your data. Defaulting back to null defeats the purpose.
Fix 4 โ Trace Where null Comes From
When the source isn't obvious, dump the value right before the decode and kill execution:
<?php
var_dump($raw); // Is it null? false? An empty string?
die();
$data = json_decode($raw, true);
Once you see the actual value, the cause usually becomes clear:
file_get_contents()returnedfalseโ wrong path, permission denied, or a network timeout$_POST['json_data']is undefined โ wrong field name, or the client sentapplication/x-www-form-urlencodedinstead ofapplication/json- DB column is
NULLโ the row exists but that field was never populated - cURL response body is empty โ the server returned a 4xx/5xx with no body
For cURL specifically, check the HTTP status code too โ a 401 or 500 response often has an empty body:
<?php
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
throw new RuntimeException("API request failed with HTTP $httpCode");
}
$data = json_decode($response, true);
Fix 5 โ Wrap It in a Helper for Consistent Handling
PHP 8 upgraded this from a warning to a full TypeError, which makes it easier to spot in logs. Either way, if you're decoding JSON in multiple places, centralise it:
<?php
function safe_json_decode(?string $json, bool $assoc = true): mixed
{
if ($json === null || $json === '') {
return null;
}
$data = json_decode($json, $assoc);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \InvalidArgumentException('Invalid JSON: ' . json_last_error_msg());
}
return $data;
}
Every decode in your codebase now goes through one function. Errors surface immediately with a useful message instead of propagating as mysterious null values three layers deep.
Verify the Fix
Quick sanity check in a scratch file:
<?php
// Should return the array without errors
$result = json_decode('{"key": "value"}', true);
var_dump($result); // array(1) { ["key"] => string(5) "value" }
// Null guard should produce an empty array
$result = json_decode(null ?? '{}', true);
var_dump($result); // array(0) {}
In a Laravel or Symfony app, run your test suite or hit the endpoint directly:
curl -X POST https://yourapp.test/api/endpoint \
-H 'Content-Type: application/json' \
-d '{"test": true}'
Also test the unhappy path โ send an empty body and confirm your guard returns a proper 400 instead of crashing.
Prevention
The real fix happens upstream, not at the decode call itself. Any JSON coming from an API, a file, or user input should be validated before it touches json_decode(). External services are especially unreliable โ they return empty bodies on auth failures, malformed JSON on partial responses, and plain HTML error pages when something breaks on their end.
When debugging malformed JSON, the JSON Formatter & Validator on ToolCraft is worth bookmarking โ paste the raw string and it pinpoints the exact syntax error immediately. Runs entirely in your browser, nothing uploaded.
Habits that make this bug nearly impossible to hit:
- Enable strict types (
declare(strict_types=1);) โ PHP 8 throwsTypeErrorbefore reachingjson_decode()if you pass the wrong type - Use typed class properties โ if
$this->jsonDatais declared asstring, assigningnullfails at the assignment, not three calls later - Log raw responses before decoding during development โ you'll catch empty or malformed responses the moment they appear
- Write unit tests that send
null, empty string, and broken JSON to every endpoint that decodes input โ make the edge cases explicit

