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

実装・エラー解決

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

AIによる要約

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

新人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でも修正箇所は異なります。レスポンスのステータスだけでなく、ログに出た例外クラス名と直前のメッセージを確認してください。

Networkの送信値とBad Requestの例外名をつなげる

400 Bad Requestの原因を調べるときは、HTTPステータスと、ログに出る具体的な例外クラス名を分けて読みます。例として、@PathVariable long orderIdへ文字列を送ってしまった場合を追います。

見る場所観測する内容判断
ブラウザのNetworkRequest 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なし)400MissingServletRequestParameterException
GET /orders/abc400MethodArgumentTypeMismatchException:orderId
POST /api/orders、JSON:{"customerId":10,"quantity":"二個"}400HttpMessageNotReadableException:Integerへ読めない
POST /api/orders、JSON:{"customerId":10,"quantity":0}400MethodArgumentNotValidException:quantity
GET /orders?status=WAITING、GET /orders/10200入力を受け取る確認用Controllerで成功
POST /api/orders、JSON:{"customerId":10,"quantity":2}200JSONを読み取り、制約も通過

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トークン
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の入力契約が分かりやすくなります。

必須パラメータの有無に加えて、例外の種類も確かめます。次は@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入門」の表紙
商品画像:Amazon.co.jp
PR Java新人・若手向け

作って学ぶ 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のフォーム入力・バリデーション実践ガイドで整理できます。

-実装・エラー解決
-,