$_POST is filled only for POST and only for the two form encodings. A JSON body must be read from the php://input stream and decoded; a form body on PUT, PATCH or DELETE is parsed by request_parse_body(), new in PHP 8.4, which returns [$post, $files] shaped like the superglobals.
<?php
header('Content-Type: application/json');
try {
$data = match (true) {
str_starts_with($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') =>
json_decode(file_get_contents('php://input'), true, 16, JSON_THROW_ON_ERROR),
$_SERVER['REQUEST_METHOD'] === 'POST' => $_POST, // PHP already parsed the body
default => request_parse_body()[0], // PUT, PATCH, DELETE (PHP 8.4+)
};
} catch (JsonException | RequestParseBodyException $e) {
http_response_code(400);
$data = ['error' => $e->getMessage()];
}
echo json_encode([$_SERVER['REQUEST_METHOD'], $_POST, $data]), "\n";Output
$ curl -s -H 'Content-Type: application/json' -d '{"book":101,"rating":5}' $U/api.php
["POST",[],{"book":101,"rating":5}]
$ curl -s -X PUT -F book=102 -F 'tags[]=clear' $U/api.php
["PUT",[],{"book":"102","tags":["clear"]}]$_POST (the middle element) stayed empty both times, and a truncated JSON body produced a 400 with {"error":"Syntax error"}. A body can be read only once: on a form POST, request_parse_body() returns empty arrays because PHP already consumed it. Its options array overrides post_max_size and the other input limits for one call; a JSON body makes it throw, hence the type check first. JSON, cURL and HTTP APIs covers JSON APIs.