Spring Bootの400 Bad Requestを切り分ける記事のアイキャッチ。Java道場風の空間で若手エンジニアがControllerとJSONのエラーを調査するイラスト。

Spring

Spring Bootで400 Bad Requestになる原因|Controllerに値が入らない時の確認順

AIによる要約

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

新人SE
新人SE
画面から送信すると400になり、Controllerの先頭に置いたブレークポイントにも止まりません。どこを見ればよいですか?
ポンコツSE
ポンコツSE
Controllerへ値を渡す前の変換で失敗している可能性があります。まずレスポンス本文ではなく、送信内容とサーバーログの例外名を対応させましょう。

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パターン

原因例外・現象の例最初に見る場所
必須値がないMissingServletRequestParameterExceptionURL・Formのname
型へ変換できないTypeMismatchException送信値と引数型
JSONを読めないHttpMessageNotReadableExceptionJSON構文・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 = falsedefaultValueを検討します。ただし、注文登録の顧客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トークン
404URLに対応するものがないパス、ID、ハンドラ
405HTTPメソッドが違うGETとPOST
415Content-Typeを扱えないapplication/json
500サーバー処理中の例外Service、DB、外部API

たとえばPOSTをGETで呼んだ場合は、通常400ではなく405です。ステータスを正確に読むだけで、Controller引数より前にURLやHTTPメソッドを直すべきだと判断できます。

Controllerに値が入らない時の確認順

  1. ブラウザのNetworkまたはAPIクライアントでステータスを確認する
  2. HTTPメソッドとURLがControllerのMappingに合うか確認する
  3. Content-Typeと送信形式が合うか確認する
  4. 実際のQuery、Path、Form、JSON本文を確認する
  5. Controllerの引数名・型・アノテーションと照合する
  6. サーバーログの最初の例外名と原因メッセージを確認する
  7. Validationのフィールドエラーを確認する
  8. 同じリクエストを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入門」の表紙
商品画像:Amazon.co.jp
PR Java新人・若手向け

作って学ぶ 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になる条件をテストへ残しましょう。

-Spring
-, , , ,