You build an API in PHP, the client POSTs JSON, and $_POST is empty. This is not a bug — it is documented behaviour. Here is why it happens, how to read the body correctly, and the error handling that matters once real traffic hits the endpoint.
Why $_POST is empty
The PHP manual defines $_POST as "an associative array of variables passed to the current script via the HTTP POST method when using application/x-www-form-urlencoded or multipart/form-data as the HTTP Content-Type in the request". PHP only parses the request body into $_POST for those two content types.
- ▸application/x-www-form-urlencoded — lands in $_POST
- ▸multipart/form-data — lands in $_POST (files go to $_FILES)
- ▸application/json — does not
- ▸application/xml and anything else — does not
The manual is explicit: to read POST data sent with other content types such as application/json or application/xml, php://input must be used. fetch and axios send JSON as application/json by default, which is exactly the case that falls outside $_POST.
Reading the body with php://input
Read the raw request body from php://input, then hand it to json_decode.
<?php
// Raw body from the request
$raw = file_get_contents('php://input');
// true returns an associative array; omit it (or false) for stdClass
$data = json_decode($raw, true);
$name = $data['name'] ?? null;Do not let malformed JSON through silently
This is where implementations differ in quality. json_decode returns null on failure, but null is ambiguous: the input may have literally been the string "null", parsing may have failed, or the payload may have exceeded the nesting limit. The return value alone cannot tell you which.
JSON_THROW_ON_ERROR, added in PHP 7.3.0, makes json_decode throw a JsonException instead. For an endpoint fed by external input, that should be the default.
<?php
header('Content-Type: application/json; charset=utf-8');
$raw = file_get_contents('php://input');
try {
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
http_response_code(400);
echo json_encode(['error' => 'invalid JSON'], JSON_THROW_ON_ERROR);
exit;
}
if (!is_array($data)) {
http_response_code(400);
echo json_encode(['error' => 'object expected'], JSON_THROW_ON_ERROR);
exit;
}
$name = $data['name'] ?? null;Valid JSON and the shape you expected are two different things. Check the decoded structure, then validate individual values.
When json_validate() helps — and when it hurts
PHP 8.3.0 added json_validate(), which checks syntax without building the decoded structure. The manual explicitly warns against calling it immediately before json_decode: doing so parses the same string twice.
<?php
// Wasteful: parses the string twice
if (json_validate($raw)) {
$data = json_decode($raw, true);
}
// Better: if you need the decoded value, the flag alone is enough
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);json_validate() earns its place when you only need to know whether the payload is well formed — for example before forwarding it to a queue or writing it to disk without decoding it.
php://input is unavailable for multipart/form-data
An easy constraint to miss: per the manual, php://input is not available in POST requests with enctype="multipart/form-data" when the enable_post_data_reading option is enabled. Designs that send a file and a JSON body together run straight into this.
- ▸Keep multipart/form-data and carry the JSON in one field, read via $_POST
- ▸Or split file upload into a separate endpoint from the JSON body
- ▸Or branch on Content-Type and handle each format explicitly
Branching on Content-Type
<?php
function read_input(): array
{
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
// Parameters may follow, e.g. 'application/json; charset=utf-8'
if (str_starts_with($contentType, 'application/json')) {
$raw = file_get_contents('php://input');
if ($raw === '' || $raw === false) {
return [];
}
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
return is_array($data) ? $data : [];
}
// urlencoded / multipart are already parsed into $_POST
return $_POST;
}Summary
- ✓$_POST is populated only for application/x-www-form-urlencoded and multipart/form-data
- ✓For application/json, read the body with file_get_contents('php://input')
- ✓json_decode with true returns an array; omitted returns stdClass
- ✓Add JSON_THROW_ON_ERROR for external input and catch JsonException to return 400
- ✓Use json_validate() only when you will not decode the value right away
- ✓php://input cannot be read for multipart/form-data — decide the split up front
