AIによる要約
Spring Bootの400 Bad Requestは、必須パラメータ不足・型変換・JSON読み取り・入力検証のどこで失敗したかを、例外名と送信値から切り分けます。Controllerのブレークポイントに止まらない場合も、Networkで実際の要求を見て、同じ要求のログと照合してください。/orders/abcを送ったときの例外から、修正後の確認まで具体的に追います。


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でも修正箇所は異なります。レスポンスのステータスだけでなく、ログに出た例外クラス名と直前のメッセージを確認してください。
Networkの送信値とBad Requestの例外名をつなげる
400 Bad Requestの原因を調べるときは、HTTPステータスと、ログに出る具体的な例外クラス名を分けて読みます。例として、@PathVariable long orderIdへ文字列を送ってしまった場合を追います。
| 見る場所 | 観測する内容 | 判断 |
|---|---|---|
| ブラウザのNetwork | Request URLの末尾が/orders/abc、Request MethodがGET、Statusが400 | 画面が実際に送った値はabc |
| Controllerの定義 | @GetMapping("/orders/{orderId}") と @PathVariable long orderId | この部分は数値として受け取る |
| 同じ要求のサーバーログ | MethodArgumentTypeMismatchException。orderId、String、long、abcという情報 | Controllerを実行する前の型変換で失敗 |
| 修正後の要求 | 存在するIDを使って/orders/10へGET | 型変換を通過できる。以後の存在確認や権限は別に確認 |
検証で出たログの主要部分は次の通りです。時刻やロガー名は省略しています。Springのバージョンによって文面は変わるため、全文の一致より、例外名・対象項目・送信値・期待する型を読みます。
MethodArgumentTypeMismatchException: Method parameter 'orderId': Failed to convert value of type 'java.lang.String' to required type 'long'; For input string: "abc"
文字列を0へ置き換えて処理を続ける修正はしません。リンクや画面側でIDではなく別の文字列を組み立てていないかを確認し、仕様に合うIDを送ります。ブレークポイントへ止まらない理由も、この型変換がController本体より先に行われることで説明できます。
4種類の400を同じ条件で再現する
検証環境:Java 17.0.8、Spring Boot 3.5.16、Hibernate Validator 8.0.1.Final。以下の入力と結果は、実際にテストして確認したものです。プロジェクトの独自Validatorや例外処理を加えた場合は、その設定でも確認してください。
次の表は、statusを必須にしたGET /orders、数値のGET /orders/{orderId}、この記事の@Valid @RequestBody OrderRequestを受けるPOST /api/ordersをMockMvcで実行した結果です。APIのquantityには@Min(1)を付けています。
| 送信した要求 | HTTP | 確認した例外・項目 |
|---|---|---|
| GET /orders(statusなし) | 400 | MissingServletRequestParameterException |
| GET /orders/abc | 400 | MethodArgumentTypeMismatchException:orderId |
| POST /api/orders、JSON:{"customerId":10,"quantity":"二個"} | 400 | HttpMessageNotReadableException:Integerへ読めない |
| POST /api/orders、JSON:{"customerId":10,"quantity":0} | 400 | MethodArgumentNotValidException:quantity |
| GET /orders?status=WAITING、GET /orders/10 | 200 | 入力を受け取る確認用Controllerで成功 |
| POST /api/orders、JSON:{"customerId":10,"quantity":2} | 200 | JSONを読み取り、制約も通過 |
JSONのPOSTにはContent-Type: application/jsonを指定しました。正常時の200は、受け取った値を返す確認用Controllerの結果です。実際の記事中のServiceによるDB処理や、その成功ステータスまで検証したものではありません。
原因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の入力契約が分かりやすくなります。
必須パラメータの有無に加えて、例外の種類も確かめます。次は@SpringBootTestと@AutoConfigureMockMvcを使うJUnit 5テストの抜粋です。assertThatはAssertJ、isBadRequestなどはMockMvcの標準Matcherです。
@Test
void 数値でないIDなら型変換エラーになる() throws Exception {
MvcResult result = mockMvc.perform(get("/orders/abc"))
.andExpect(status().isBadRequest())
.andReturn();
assertThat(result.getResolvedException())
.isInstanceOf(MethodArgumentTypeMismatchException.class);
MethodArgumentTypeMismatchException error =
(MethodArgumentTypeMismatchException) result.getResolvedException();
assertThat(error.getName()).isEqualTo("orderId");
assertThat(error.getValue()).isEqualTo("abc");
}
@Test
void 読めるJSONでも数量ゼロはValidationエラーになる() throws Exception {
MvcResult result = mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"customerId\":10,\"quantity\":0}"))
.andExpect(status().isBadRequest())
.andReturn();
assertThat(result.getResolvedException())
.isInstanceOf(MethodArgumentNotValidException.class);
MethodArgumentNotValidException error =
(MethodArgumentNotValidException) result.getResolvedException();
assertThat(error.getBindingResult().getFieldErrors())
.extracting("field").containsExactly("quantity");
}
主なimportはorg.springframework.test.web.servlet.MvcResult、org.springframework.http.MediaType、org.springframework.web.method.annotation.MethodArgumentTypeMismatchException、org.springframework.web.bind.MethodArgumentNotValidExceptionです。読めないJSONのケースでは、同じ方法でHttpMessageNotReadableExceptionを確認します。ValidationとJSON読取の失敗を同じ原因として修正しないことが大切です。
現場レビューでよくある指摘
// レビューコメント例 検索条件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でご確認ください。
MockMvcだけで403・400になるなら、Securityを有効にしたPOSTテストの失敗条件と修正例でリクエスト条件をそろえます。原因例外は分かっても修正箇所を選べない場合は、Springの400でCaused byから自作コードへ戻る実測例へ進んでください。Controller未実行で自作コードの行がない場合も扱います。
この記事とあわせて読みたい
まとめ
Spring Bootの400 Bad Requestは、必須値不足、型変換、JSON読み取り、Validationのどこで失敗したかを分けて考えます。Controllerへ到達しない場合は、メソッド内部より先に引数を作る過程を確認してください。
ステータス、HTTPメソッド、URL、Content-Type、送信値、例外名の順で確認すると、調査が推測ではなくなります。修正後は同じ入力をMockMvcで再現し、正常系と400になる条件をテストへ残しましょう。
400の原因を切り分けた後に、入力エラーをどう画面へ返し、何をテストするかは、Spring Bootのフォーム入力・バリデーション実践ガイドで整理できます。
