できない.dev

curl で JSON を POST してもサーバー側で受け取れない

-d は既定で Content-Type: application/x-www-form-urlencoded を送るため、JSON を渡してもサーバーはフォーム送信として解釈する。
ヘッダーを明示するか、curl 7.82.0 以降なら --json を使う。

公開: 更新:

症状できない
curl -d '{"name":"curl","count":1}' http://localhost:8000/items
{"error": "Content-Type must be application/json (got application/x-www-form-urlencoded)", "raw": "{\"name\":\"curl\",\"count\":1}"}

2026-10-03 に python:3.12(Python 3.12.15 / Git 2.47.3) で実際に打って取った出力。検証環境の詳細

要約

curl -d '{"name":"curl"}' https://api.example.com/items が 400 や 415 で返る、あるいはサーバー側で本文が空に見えるとき、多くは Content-Type がフォーム送信のままになっていることが原因である。

-d(--data)はブラウザのフォーム送信を再現するオプションで、指定がなければ application/x-www-form-urlencoded を送る。
JSON を本文に入れても、ヘッダーがフォームのままならサーバー側の JSON パーサは動かない。

実行例

curl 8.14.1 で -d だけを付けて JSON を送ると、サーバーには application/x-www-form-urlencoded で届いたと返され、-X POST だけでボディを付けない場合は Content-Type も本文も空だった。-H "Content-Type: application/json" を付けるか --json を使うと JSON として受け取られ、-d @body.json は改行を落とした raw で届く一方、--data-binary と --json @body.json は改行が残ったまま届いている。

$ curl -d '{"name":"curl","count":1}' http://localhost:8000/items
{"error": "Content-Type must be application/json (got application/x-www-form-urlencoded)", "raw": "{\"name\":\"curl\",\"count\":1}"}
$ curl -X POST http://localhost:8000/items
{"error": "Content-Type must be application/json (got None)", "raw": ""}
$ curl -X POST http://localhost:8000/items -H "Content-Type: application/json" -d '{"name":"curl","count":1}'
{"received": {"name": "curl", "count": 1}, "raw": "{\"name\":\"curl\",\"count\":1}"}
$ curl --version | head -1
curl 8.14.1 (x86_64-pc-linux-gnu) libcurl/8.14.1 OpenSSL/3.5.7 zlib/1.3.1 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.2 libssh2/1.11.1 nghttp2/1.64.0 nghttp3/1.8.0 librtmp/2.3 OpenLDAP/2.6.10
$ curl --json '{"name":"curl","count":1}' http://localhost:8000/items
{"received": {"name": "curl", "count": 1}, "raw": "{\"name\":\"curl\",\"count\":1}"}
$ curl -H "Content-Type: application/json" -d @body.json http://localhost:8000/items
{"received": {"name": "curl", "count": 1}, "raw": "{  \"name\": \"curl\",  \"count\": 1}"}
$ curl -H "Content-Type: application/json" --data-binary @body.json http://localhost:8000/items
{"received": {"name": "curl", "count": 1}, "raw": "{\n  \"name\": \"curl\",\n  \"count\": 1\n}\n"}
$ curl --json @body.json http://localhost:8000/items
{"received": {"name": "curl", "count": 1}, "raw": "{\n  \"name\": \"curl\",\n  \"count\": 1\n}\n"}
$ curl -v --json '{"name":"curl"}' http://localhost:8000/items
* Host localhost:8000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8000...
* connect to ::1 port 8000 from ::1 port 51408 failed: Connection refused
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000
* using HTTP/1.x
> POST /items HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.14.1
> Content-Type: application/json
> Accept: application/json
> Content-Length: 15
> 
} [15 bytes data]
* upload completely sent off: 15 bytes
* HTTP 1.0, assume close after body
< HTTP/1.0 201 Created
< Server: BaseHTTP/0.6 Python/3.12.15
< Date: Sat, 03 Oct 2026 01:48:55 GMT
< Content-Type: application/json
< Content-Length: 61
< 
{ [61 bytes data]
* shutting down connection #0
{"received": {"name": "curl"}, "raw": "{\"name\":\"curl\"}"}

— 2026-10-03 時点の出力

検証環境Python 3.12.15 / Git 2.47.3 / 2026-10-03 検証
検証日
実行環境
python:3.12 Debian GNU/Linux 13 (trixie)
バージョン
  • Python 3.12.15
  • Git 2.47.3

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

解決策

1. Content-Type を明示する

curl -X POST https://api.example.com/items \
  -H "Content-Type: application/json" \
  -d '{"name":"curl","count":1}'

-d を使うと curl は自動的に POST になるため、-X POST は省略できる。

2. curl 7.82.0 以降は --json を使う

--json は --data-binary と Content-Type: application/json / Accept: application/json の 2 つのヘッダー指定をまとめた短縮形である。

curl --json '{"name":"curl","count":1}' https://api.example.com/items

利用中の curl が対応しているかは版数で判断する。

curl --version | head -1

3. Windows ではファイル渡しにする

クオートの解釈差を避けるには、本文をファイルに書いて @ で読ませるのが確実である。

curl --json @body.json https://api.example.com/items

--json が使えない場合は --data-binary にヘッダーを添える。--data-binary は -d と違い改行や復帰をそのまま送る。

curl -H "Content-Type: application/json" --data-binary @body.json https://api.example.com/items

4. 送信内容を目で確認する

curl -v --json '{"name":"curl"}' https://api.example.com/items

> で始まる行が curl の送ったリクエストである。Content-Type: application/json が出ているか、本文が壊れていないかをここで確認する。
ヘッダーが正しいのに 4xx が返るなら、原因は curl 側ではなくスキーマ側にある。

よくある原因

  1. Content-Type を指定していない。
    curl 自身はボディの中身を検査しないので、JSON かどうかは判定してくれない。
  2. シェルによるクオートの差。
    Windows のコマンドプロンプトはシングルクオートを引用符として扱わないため、'{"name":"curl"}' がそのまま文字列として渡り、JSON が壊れる。
  3. -d @body.json を使っている。-d はファイルから読むときに改行・復帰・NULL バイトを取り除くため、整形済み JSON が 1 行に潰れる。
    JSON としては有効なので気づきにくいが、改行を保ちたい用途では問題になる。
  4. -X POST だけを書いてボディを渡していない。
    メソッドは POST になるが本文は空である。

この手順で直った?