PHPでAPIを作り、クライアントからJSONをPOSTしたのに $_POST が空だった——という詰まり方はよくあります。これはバグではなく仕様です。原因と正しい受け取り方、そして本番で効いてくるエラー処理まで整理します。
$_POST が空になる理由
PHP公式マニュアルは $_POST を「HTTP Content-Type が application/x-www-form-urlencoded または multipart/form-data のときにPOSTメソッドで渡された変数の連想配列」と定義しています。つまり、PHPがリクエストボディを自動的にパースして $_POST に詰めてくれるのは、この2つのContent-Typeに限られます。
- ▸application/x-www-form-urlencoded … $_POST に入る
- ▸multipart/form-data … $_POST に入る(ファイルは $_FILES)
- ▸application/json … $_POST には入らない
- ▸application/xml やその他 … 同じく入らない
マニュアルにも「これ以外のContent-Type(application/json や application/xml など)で送られたPOSTデータを読むには php://input を使わなければならない」と明記されています。fetchやaxiosはデフォルトでJSONを application/json として送るため、この条件から外れます。
php://input で受け取る
リクエストボディの生データは php://input から読み出します。読み出した文字列を json_decode に渡せば、PHPの配列やオブジェクトとして扱えます。
<?php
// リクエストボディを生のまま取得する
$raw = file_get_contents('php://input');
// 第2引数 true で連想配列として受け取る(false / 省略なら stdClass)
$data = json_decode($raw, true);
$name = $data['name'] ?? null;json_decode の第2引数(associative)を true にすると連想配列、省略または false にすると stdClass オブジェクトが返ります。配列で扱いたいなら true を明示してください。
壊れたJSONを黙って通さない
ここが実務で一番差が出るところです。json_decode は失敗すると null を返しますが、この null は3つの意味を持ちます。「入力そのものが null という文字列だった」「パースに失敗した」「ネストの上限を超えた」——戻り値だけでは区別できません。
PHP 7.3.0 で追加された JSON_THROW_ON_ERROR を渡すと、失敗時に JsonException が投げられます。外部から来る入力を扱うAPIでは、こちらを既定にするほうが安全です。
<?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;
}
// ここから先は「JSONとして妥当」が保証された状態
$name = $data['name'] ?? null;JSONとして妥当であることと、期待した構造であることは別物です。上の例のように is_array で受け取った形を確認し、値は ?? やバリデーションで個別に検証してください。
json_validate() を使うべき場面・使うべきでない場面
PHP 8.3.0 で json_validate() が追加され、デコード結果を組み立てずに構文の妥当性だけを判定できるようになりました。ただし公式マニュアルは、json_decode の直前に json_validate を呼ぶことを明確に戒めています。同じ文字列を2回パースすることになるからです。
<?php
// 悪い例: 二重にパースしている
if (json_validate($raw)) {
$data = json_decode($raw, true);
}
// 良い例: 復号結果が要るなら JSON_THROW_ON_ERROR だけで足りる
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);json_validate() が向くのは「妥当性だけ確認して中身は今すぐ使わない」ケースです。受信したペイロードをそのままキューやファイルへ流す前のチェックなどが該当します。
multipart/form-data では php://input が使えない
見落としやすい制約です。公式マニュアルによると、enable_post_data_reading が有効な場合、enctype="multipart/form-data" のPOSTリクエストでは php://input を利用できません。ファイルアップロードと一緒にJSONを送る設計にすると、この壁に当たります。
- ▸ファイル送信を伴う場合は multipart/form-data のまま、JSONは1つのフィールドに入れて $_POST 経由で受け取る
- ▸JSONボディで受けたい場合はファイルを別エンドポイントに分ける
- ▸どちらの形式で来るかを Content-Type で分岐させ、処理を切り替える
Content-Typeで分岐する実装例
同じエンドポイントでフォーム送信とJSON送信の両方を受ける場合は、Content-Type を見て入口だけ切り替えると見通しが良くなります。
<?php
function read_input(): array
{
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
// '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 はPHPが $_POST に詰めてくれている
return $_POST;
}まとめ
- ✓$_POST が埋まるのは application/x-www-form-urlencoded と multipart/form-data のときだけ
- ✓application/json は file_get_contents('php://input') で生データを読む
- ✓json_decode の第2引数 true で連想配列、省略時は stdClass
- ✓外部入力には JSON_THROW_ON_ERROR を付け、JsonException を捕まえて400を返す
- ✓json_validate() は「デコード結果を使わない」ときだけ。直前に呼ぶと二重パース
- ✓multipart/form-data では php://input が読めないので設計段階で分岐を決める
