HTTP POSTでJSONを送るには
各言語で JSON を本文に載せた POST を送り、Content-Type を明示して、返ってきた 2xx のレスポンスを読むところまでの基本形を示す。
本文は原則としてその言語のシリアライザに任せ、標準ライブラリに JSON シリアライザを持たない言語だけを例外として扱う。
公開:
各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。
Python 実行確認済み
import json
import urllib.request
url = "https://jsonplaceholder.typicode.com/posts"
payload = {"title": "hello", "body": "world", "userId": 1}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url, data=data, method="POST", headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=10) as res:
created = json.loads(res.read().decode("utf-8"))
print(res.status, created["id"])標準ライブラリの urllib.request で POST を送る。
data にバイト列を渡した時点でメソッドは POST になるが、意図を読み取れるように method を明示している。
Content-Type を自分で付けるのは、urllib が既定で application/x-www-form-urlencoded を仮定し、受け側が JSON として解釈しないためである。
うまくいかない時: Python で「json.decoder.JSONDecodeError: Expecting value」が解消できない
JavaScript 実行確認済み
const url = "https://jsonplaceholder.typicode.com/posts";
async function main() {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "hello", body: "world", userId: 1 }),
signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
throw new Error(`unexpected status: ${res.status}`);
}
const created = await res.json();
console.log(res.status, created.id);
}
main();Node.js 18 以降のグローバル fetch で送れるので外部パッケージは要らない。
body には JSON.stringify した文字列を渡し、Content-Type を明示する。
res.ok を先に見るのは、fetch が 4xx / 5xx でも reject せず解決するため、確認しないとエラー応答を作成結果として読み進めてしまうからである。
うまくいかない時: Node.js で「fetch is not defined」が解決できない(古い Node) / Node.js で fetch が「self-signed certificate in certificate chain」で接続できない
TypeScript 実行確認済み
type NewPost = { title: string; body: string; userId: number };
type CreatedPost = NewPost & { id: number };
const url = "https://jsonplaceholder.typicode.com/posts";
async function createPost(input: NewPost): Promise<CreatedPost> {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
throw new Error(`unexpected status: ${res.status}`);
}
return (await res.json()) as CreatedPost;
}
async function main(): Promise<void> {
const created = await createPost({ title: "hello", body: "world", userId: 1 });
console.log(created.id, created.title);
}
main();送る形と返る形を別々の型で宣言しておくと、リクエスト本文の作り間違いをコンパイル時に潰せる。
res.json() の戻り値は unknown 相当なので as で受けているが、実行時の検証にはならない。
外部 API が相手なら zod などのスキーマ検証を挟むほうが安全である。
Go 静的確認
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
type NewPost struct {
Title string `json:"title"`
Body string `json:"body"`
UserID int `json:"userId"`
}
func main() {
body, err := json.Marshal(NewPost{Title: "hello", Body: "world", UserID: 1})
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 10 * time.Second}
res, err := client.Post(
"https://jsonplaceholder.typicode.com/posts",
"application/json",
bytes.NewReader(body),
)
if err != nil {
panic(err)
}
defer res.Body.Close()
if res.StatusCode/100 != 2 {
panic(fmt.Sprintf("unexpected status: %d", res.StatusCode))
}
var created struct{ ID int }
if err := json.NewDecoder(res.Body).Decode(&created); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, created.ID)
}json.Marshal した結果を bytes.NewReader で io.Reader に包んで渡す。
client.Post の第2引数が Content-Type なので、ヘッダを別に組み立てる必要はない。
既定の http.DefaultClient にはタイムアウトが無いため、応答しない相手で詰まらないよう http.Client を自分で組み立てている。
Rust 静的確認
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let payload = json!({ "title": "hello", "body": "world", "userId": 1 });
let created: serde_json::Value = reqwest::blocking::Client::new()
.post("https://jsonplaceholder.typicode.com/posts")
.json(&payload)
.send()?
.error_for_status()?
.json()?;
println!("{}", created["id"]);
Ok(())
}reqwest の json() は本文のシリアライズと Content-Type の付与をまとめて行うため、ヘッダを手で書く必要がない。
error_for_status() を挟むのは、4xx / 5xx を Err に変換してエラー応答を作成結果として読み進めないようにするためである。
json 機能を有効にしないとこのメソッドは生えない。
Java 静的確認
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class Main {
public static void main(String[] args) throws Exception {
String json = """
{"title":"hello","body":"world","userId":1}""";
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest req = HttpRequest.newBuilder(URI.create("https://jsonplaceholder.typicode.com/posts"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() / 100 != 2) {
throw new RuntimeException("unexpected status: " + res.statusCode());
}
System.out.println(res.statusCode());
System.out.println(res.body());
}
}Java 11 以降の java.net.http.HttpClient で POST を送れる。
BodyPublishers.ofString に本文の文字列を渡し、Content-Type を明示する。
この例だけ JSON をテキストブロックで直接書いているのは、Java の標準ライブラリに JSON シリアライザが無いためである。
値が固定のうちはこれで足りるが、動的な値を埋めるなら Jackson などに任せる。
手で連結すると引用符や改行のエスケープで壊れる。
C# 実行確認済み
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
record NewPost(string Title, string Body, int UserId);
record CreatedPost(int Id, string Title);
class Program
{
static readonly HttpClient client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(10)
};
static async Task Main()
{
var input = new NewPost("hello", "world", 1);
using var res = await client.PostAsJsonAsync(
"https://jsonplaceholder.typicode.com/posts", input);
res.EnsureSuccessStatusCode();
var created = await res.Content.ReadFromJsonAsync<CreatedPost>();
Console.WriteLine($"{(int)res.StatusCode} {created?.Id}");
}
}System.Net.Http.Json の PostAsJsonAsync を使うと、シリアライズと Content-Type の付与を任せられる。
HttpClient を static フィールドで使い回すのは、リクエストごとに生成して破棄すると TIME_WAIT のソケットが積み上がって SocketException に至るためである。
既定のプロパティ名は camelCase へ変換されるので、title / body / userId として送られる。
つまずき
POST が通らないときにまず疑うのは Content-Type である。
本文は JSON なのにヘッダを付け忘れると、受け側は既定の form 形式として解釈し、400 を返すか、全フィールドが空のまま 200 を返す。
後者は「エラーが出ないのにデータが入らない」という形で表面化するので厄介である。
次に多いのがステータスの確認漏れで、JavaScript の fetch は 4xx / 5xx でも Promise を解決するため、res.ok を見ないとエラー応答を作成結果として読み進めてしまう。
作成系の API は 200 ではなく 201 を返すことも多いので、200 との等値比較ではなく 2xx の範囲で判定するほうがよい。
本文は文字列連結で組み立てない
JSON の本文をテンプレート文字列で組み立てると、値に引用符・改行・バックスラッシュが混じった瞬間に壊れる。
ユーザー入力が入る場面では現実に起きるうえ、壊れた本文は受け側で 400 になるだけで、どの値が原因かは分からない。
各言語のシリアライザ(Python の json.dumps、Go の json.Marshal、C# の PostAsJsonAsync など)に任せれば、エスケープは処理系が担保する。
ここに載せたサンプルがどれも辞書やレコードから作っているのはそのためである。
リトライするなら冪等性を先に確認する
GET と違い POST は既定では冪等ではない。
タイムアウトしたリクエストをそのまま再送すると、サーバ側では 1 回目も届いていて二重登録になることがある。
応答が返らなかったときに何が起きるかは相手の API 次第なので、無条件のリトライを入れる前に仕様を確認したい。
API が Idempotency-Key のようなヘッダを受け付けるならそれを付ける。
受け付けないなら、リトライは接続確立前の失敗に限る、あるいは作成後に一覧を引いて重複を確認する、といった設計にする。