AIによる要約
Spring Bootの400 Bad Requestは、Controllerの処理へ入る前にリクエストの読み取りが失敗したときによく発生します。主な原因は必須パラメータ不足、数値や日付への型変換失敗、壊れたJSON、入力チェックエラーです。闇雲にアノテーションを追加せず、HTTPメソッド、URL、Content-Type、実際の送信値、例外名の順に確認すると切り分けやすくなります。


400 Bad Requestは「サーバーへ到達していない」という意味ではありません。Spring MVCまでは届いたものの、リクエストをControllerの引数へ変換できない、または入力値を受け付けられない場合に返されます。
現場では「400だからJSONが悪い」と決めつけ、Form送信や必須パラメータの確認が遅れることがあります。原因候補をHTTPの入口から順に確認すれば、ログの読み方がまだ不慣れな新人でも調査範囲を絞れます。
この記事のポイント
- 400はController実行前のバインド・変換・検証で起きやすい
- 必須パラメータ、型、JSON、Validationを分けて確認する
- ブラウザのNetworkとサーバーログを同じリクエストで照合する
- 405・415・401・403・500を400と混同しない
400 Bad Requestが返る代表的な4パターン
| 原因 | 例外・現象の例 | 最初に見る場所 |
|---|---|---|
| 必須値がない | MissingServletRequestParameterException | URL・Formのname |
| 型へ変換できない | TypeMismatchException | 送信値と引数型 |
| JSONを読めない | HttpMessageNotReadableException | JSON構文・Content-Type |
| 入力チェックに失敗 | MethodArgumentNotValidExceptionなど | エラー項目・制約 |
同じ400でも修正箇所は異なります。レスポンスのステータスだけでなく、ログに出た例外クラス名と直前のメッセージを確認してください。
原因1:必須の@RequestParamが送られていない
@GetMapping("/orders")
public String list(
@RequestParam String status,
Model model) {
model.addAttribute("orders", orderService.findByStatus(status));
return "orders/list";
}
このControllerではstatusが必須です。/ordersだけへアクセスすると値が存在しないため、Controller本体を実行する前に400になります。
NG: /orders OK: /orders?status=WAITING
任意の検索条件ならrequired = falseやdefaultValueを検討します。ただし、注文登録の顧客IDのように本当に必須の値まで任意にしてはいけません。
@GetMapping("/orders")
public String list(
@RequestParam(required = false) String status,
Model model) {
model.addAttribute("orders", orderService.search(status));
return "orders/list";
}
HTML Formでは、Controllerの引数名ではなくinput要素のnameが送信名です。画面に値が表示されていても、nameが違えばSpringには届きません。
<!-- NG: Controllerはstatusを待っている --> <select name="orderStatus">...</select> <!-- OK --> <select name="status">...</select>
原因2:数値や日付へ型変換できない
@GetMapping("/orders/{orderId}")
public String detail(
@PathVariable long orderId,
Model model) {
model.addAttribute("order", orderService.find(orderId));
return "orders/detail";
}
/orders/abcへアクセスすると、abcをlongへ変換できません。メソッド内でorderIdを使う前に型変換が失敗するため、ブレークポイントへ到達しないことがあります。
日付でも同じです。LocalDateを受けるなら、画面が送る形式とSpringが期待する形式を合わせます。
@GetMapping("/orders")
public String list(
@RequestParam
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate orderDate,
Model model) {
return "orders/list";
}
OK: orderDate=2026-07-23 NG: orderDate=2026/07/23 NG: orderDate=7月23日
形式を変える場合は、Controllerだけでなく画面、API仕様書、テストデータも揃えます。変換できない値を0や今日の日付へ黙って置き換えると、入力ミスに気づけなくなります。
原因3:@RequestBodyのJSONを読み取れない
public record OrderRequest(
Long customerId,
Integer quantity) {
}
@PostMapping("/api/orders")
@ResponseBody
public OrderResponse create(
@RequestBody OrderRequest request) {
return orderService.create(request);
}
JSONのカンマ不足、文字列の引用符不足、数値項目へ文字列を送った場合などは、HttpMessageConverterが読み取れず400になります。
// OK
{
"customerId": 10,
"quantity": 2
}
// NG: quantityを数値へ変換できない
{
"customerId": 10,
"quantity": "二個"
}
JSONを送る場合はContent-Type: application/jsonも確認します。Form形式を送っているのに@RequestBodyで受けようとしていないか、逆にJSONを@ModelAttributeで受けようとしていないかも見ます。
パスワードや個人情報を調査ログへ貼らない
リクエスト本文の確認は必要ですが、認証情報、カード情報、個人情報をチャットやチケットへそのまま貼らないでください。必要な項目だけをマスクし、現場の情報管理ルールに従います。
原因4:@Validの入力チェックに失敗している
public record OrderRequest(
@NotNull Long customerId,
@Min(1) Integer quantity) {
}
@PostMapping("/api/orders")
public ResponseEntity<OrderResponse> create(
@Valid @RequestBody OrderRequest request) {
return ResponseEntity.ok(orderService.create(request));
}
customerIdがnull、quantityが0なら、JSON構文が正しくても入力チェックで400になります。これは「JSONを読めない」のではなく、「読めたが制約に違反した」状態です。
APIでは@RestControllerAdviceなどでエラー項目を一定形式へ変換すると、フロント側と調査担当が原因を判断しやすくなります。画面FormではBindingResultを受け取り、同じ画面へ戻す構成が一般的です。
400ではないエラーを混同しない
| ステータス | 代表的な意味 | 確認例 |
|---|---|---|
| 400 | 値・形式・入力チェックが不正 | パラメータ、型、JSON |
| 401 | 認証が必要 | ログイン、トークン |
| 403 | 権限・CSRFなどで拒否 | 権限、CSRFトークン |
| 404 | URLに対応するものがない | パス、ID、ハンドラ |
| 405 | HTTPメソッドが違う | GETとPOST |
| 415 | Content-Typeを扱えない | application/json |
| 500 | サーバー処理中の例外 | Service、DB、外部API |
たとえばPOSTをGETで呼んだ場合は、通常400ではなく405です。ステータスを正確に読むだけで、Controller引数より前にURLやHTTPメソッドを直すべきだと判断できます。
Controllerに値が入らない時の確認順
- ブラウザのNetworkまたはAPIクライアントでステータスを確認する
- HTTPメソッドとURLがControllerのMappingに合うか確認する
- Content-Typeと送信形式が合うか確認する
- 実際のQuery、Path、Form、JSON本文を確認する
- Controllerの引数名・型・アノテーションと照合する
- サーバーログの最初の例外名と原因メッセージを確認する
- Validationのフィールドエラーを確認する
- 同じリクエストをMockMvcで再現する
ブレークポイントへ止まらないときは、Controllerの中ではなく「引数を作るまで」に失敗していると考えると調査しやすくなります。
MockMvcで400を再現する
@Test
void statusがなければ400になる() throws Exception {
mockMvc.perform(get("/orders"))
.andExpect(status().isBadRequest());
}
@Test
void statusがあれば画面を表示できる() throws Exception {
mockMvc.perform(get("/orders")
.param("status", "WAITING"))
.andExpect(status().isOk())
.andExpect(view().name("orders/list"));
}
修正後に正常系だけを追加するのではなく、何が不足すると400になるかもテストへ残すと、Controllerの入力契約が分かりやすくなります。
現場レビューでよくある指摘
// レビューコメント例 検索条件statusは任意ですが@RequestParamが必須になっています。 条件未指定で全件検索する仕様ならrequired=falseを検討してください。 // レビューコメント例 400をすべて「入力エラー」とだけ返しており、項目を特定できません。 APIのエラー形式に沿ってフィールド名とメッセージを返してください。 // レビューコメント例 例外対応としてquantityを0へ変換していますが、 不正入力を正常値へ置き換える仕様ではありません。400で返す方針を確認してください。
提出前のセルフチェック
- HTTPメソッド、URL、Content-Typeを確認したか
- 必須と任意のパラメータを仕様に合わせたか
- 画面のnameとControllerの受取名が一致しているか
- 数値・日付の形式をテストしたか
- 正常JSONと不正JSONの両方を確認したか
- Validationエラーの項目を利用者へ返せるか
- 機密情報をログやチケットへ残していないか
Spring MVCの入口を手を動かして学ぶ参考書
Controller、Form、JSON、Validationを小さなアプリで通して確認すると、400エラーの切り分けが早くなります。
作って学ぶ Spring Boot入門
Spring Bootを、動くWebアプリを作りながら覚える。
Controller、画面、DB連携などを一つのアプリとして実装できます。Java基礎からSpringの現場へ進む橋渡しに向く一冊です。
- Spring Bootを手を動かして学びたい
- Webアプリ開発の流れをつかみたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
Spring Bootの400 Bad Requestは、必須値不足、型変換、JSON読み取り、Validationのどこで失敗したかを分けて考えます。Controllerへ到達しない場合は、メソッド内部より先に引数を作る過程を確認してください。
ステータス、HTTPメソッド、URL、Content-Type、送信値、例外名の順で確認すると、調査が推測ではなくなります。修正後は同じ入力をMockMvcで再現し、正常系と400になる条件をテストへ残しましょう。
