Spring Bootの@RequestBodyでJSONを受け取る使い方を解説する記事のアイキャッチ。若手エンジニアがJava道場風の学習空間でAPI入力を学ぶイラスト。

実装・エラー解決

Spring Bootの@RequestBodyとは?JSONを受け取る使い方と注意点

AIによる要約

@RequestBodyは、HTTPリクエストのボディに入っているJSONをJavaオブジェクトへ変換して受け取るために使います。クエリ文字列やHTMLフォームを受けるためのものではありません。APIの入口ではRequest DTOとセットで考えると安全です。

新人SE
新人SE
JSONを受け取るControllerでは、@RequestParamではなく@RequestBodyを使うんですか?
ポンコツSE
ポンコツSE
はい。JSONはリクエストボディに入るので@RequestBodyで受けます。URLの後ろに付く条件やHTMLフォームとは受け取り方が違います。

Spring BootでAPIを作ると、@RequestBodyがよく出てきます。

利用者登録のAPIにJSONを送って400になる場合、Controllerの登録メソッドにブレークポイントを置くだけでは原因が見えません。このサンプルで、本文が読めないときと、読めたDTOが入力チェックで弾かれるときを切り分けます。

この記事のポイント

  • @RequestBodyはJSONなどのリクエストボディを受け取る
  • HTMLフォームやクエリ文字列とは受け取り方が違う
  • APIではRequest DTOを用意してEntityと分ける
  • 本文の読み取り失敗とDTOの入力チェックを区別する

実行用サンプル

正常なJSON、壊れた文法、型不一致、本文なし、DTOの入力チェックを比較します。 ソース全文、JUnitの検証コード、curlの実行ファイル、pom.xml、READMEを収録しています。JDK 17とMaven 3.9系で mvn test を実行します。Spring Boot 3.5.16のサンプルです。

@RequestBodyはリクエストボディを読む

@RequestBodyは、HTTPリクエストのボディに入っているデータをJavaオブジェクトへ変換します。Spring BootのAPIでは、JSONを受け取るときによく使います。

UserInputController.java の抜粋。JSON登録を受けるメソッド

    @PostMapping(path = "/api/users", consumes = APPLICATION_JSON_VALUE)
    public ResponseEntity<UserView> create(@Valid @RequestBody UserCreateRequest request) {
        UserView user = userService.create(request.name(), request.email(), request.notificationLimit());
        return ResponseEntity.created(URI.create("/api/users/" + user.id())).body(user);
    }

Spring MVCの HttpMessageConverter が本文を読み、Jacksonで UserCreateRequest に変換します。次に @Valid がDTOを検証し、ここまで通ると初めて create の本体からServiceが呼ばれます。

送信されるJSONの例

create-user.json の全文

{
  "name": "Suzuki",
  "email": "suzuki@example.com",
  "notificationLimit": 5
}

name、email、notificationLimit がJavaのrecordの各コンポーネントに対応します。展開先でアプリを起動し、下のJSONを送ると201と Location が返ります。このサンプルは設定で未知のプロパティをエラーにしているため、キーの綴り違いも確認できます。

UserCreateRequest.java の全文

package info.syachiku.user;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record UserCreateRequest(
        @NotBlank String name,
        @NotBlank @Email String email,
        @NotNull @Min(1) Integer notificationLimit) {}

サンプルを mvn spring-boot:run で起動し、別のターミナルで展開先へ移動して送ります。

locations.sh の抜粋。接続先の設定

base_url="${BASE_URL:-http://localhost:8080}"

locations.sh の抜粋。正常なJSONの送信コマンド

# 本文にJSONを送る
curl -si "$base_url/api/users" \
  -H 'Content-Type: application/json' \
  --data-binary @requests/create-user.json

@RequestParamとの違い

@RequestParamは、主にクエリ文字列やフォームの単発パラメータを受け取ります。JSONボディを受け取るためのものではありません。

同じ /api/users でも、GET /api/users?keyword=tanaka の keyword はURLのクエリ、POST /api/users の name はJSON本文です。ブラウザーの開発者ツールではQuery String ParametersとPayloadを見分けます。

たとえば本文が空のまま POST /api/users?name=Suzuki を送っても、クエリの name でDTOは作られません。JSON本文として指定します。

入力チェックは@Validと組み合わせる

APIでも入力チェックは必要です。Request DTOに制約を付け、Controllerで@Validを付けます。

先ほどの UserCreateRequest で @NotNull が上限の未指定を、@Min(1) が0以下を弾きます。上限の型を Integer にしたので、未指定と0を区別できます。

構文が正しく "notificationLimit":0 を送ると、0は Integer に変換できますが @Min(1) を満たしません。Spring MVCは MethodArgumentNotValidException を解決し、標準の処理でHTTP 400を返します。

Entityを直接受け取らない

APIでも、Entityを@RequestBodyで直接受けるのは避けた方がよいことが多いです。

UserService.java の抜粋。保存データの初期権限をサーバー側で決める

    UserView create(String name, String email, int notificationLimit) {
        long id = sequence.incrementAndGet();
        StoredUser user = new StoredUser(id, name, email, notificationLimit, "MEMBER");
        users.put(id, user);
        return user.view();
    }

入力用DTOには、保存データの id や role を含めません。この例では、ServiceがIDと初期権限 MEMBER を決め、応答用DTOにも role を含めません。JSONへ "role":"ADMIN" を追加すると、サンプルの未知プロパティの設定により400になり、登録は呼ばれません。

400エラーになったときの確認ポイント

確認リスト

  • Content-Type: application/json を指定し、本文も送ったか
  • JSONの項目名とRequest DTOのフィールド名が合っているか
  • この例の notificationLimit に整数を渡したか
  • JSONの文法が壊れていないか
  • 整数に変換できた値が @Min(1) を満たすか

下の4リクエストはすべて400ですが、上から順に「文法」「型変換」「必須の本文」「DTOの制約」で止まります。Content-Type が異なり415が返る場合は、まず リクエストを受けるハンドラの条件 を確認します。

body-errors.sh の抜粋。登録APIへ異なる不正入力を送るコマンド

base_url="${BASE_URL:-http://localhost:8080}"

# 文法エラー。閉じ波括弧がない
curl -si "$base_url/api/users" \
  -H 'Content-Type: application/json' \
  --data-binary '{"name":"Suzuki","email":"suzuki@example.com","notificationLimit":5'

# 型変換エラー。manyをIntegerにできない
curl -si "$base_url/api/users" \
  -H 'Content-Type: application/json' \
  --data-binary '{"name":"Suzuki","email":"suzuki@example.com","notificationLimit":"many"}'

# 本文なし。Content-TypeはJSON
curl -si "$base_url/api/users" \
  -H 'Content-Type: application/json' \
  --data-binary ''

# 制約違反。Integerには変換できるが、1以上ではない
curl -si "$base_url/api/users" \
  -H 'Content-Type: application/json' \
  --data-binary '{"name":"Suzuki","email":"suzuki@example.com","notificationLimit":0}'
送った内容Spring MVCが報告した例外直す箇所
最後の } がないHttpMessageNotReadableException閉じ波括弧を足す
notificationLimit が "many"HttpMessageNotReadableException数値の 5 を送る
JSONの指定はあるが本文が空HttpMessageNotReadableException必要な3項目をJSON本文に入れる
notificationLimit が 0MethodArgumentNotValidException1以上の整数にする

MockMvcでハンドラとして create が選ばれても、引数が確定しなければ本体に入らないことを確かめました。下は 0 のケースで、notificationLimit の Min エラーと登録Serviceの呼び出し0回を検証するテストです。

UserInputTest.java の抜粋。数値への変換後、制約で弾かれることを検証

    @Test
    void parsedJsonCanFailValidationBeforeCallingService() throws Exception {
        var result = mvc.perform(post("/api/users").contentType(APPLICATION_JSON)
                        .content("""
                                {"name":"Suzuki","email":"suzuki@example.com","notificationLimit":0}
                                """))
                .andExpect(status().isBadRequest())
                .andExpect(handler().methodName("create"))
                .andReturn();
        assertThat(result.getResolvedException()).isInstanceOfSatisfying(
                MethodArgumentNotValidException.class, error ->
                        assertThat(error.getBindingResult().getFieldError("notificationLimit").getCode())
                                .isEqualTo("Min"));
        assertThat(service.createCalls.get()).isZero();
    }

2記事の共通サンプル全体の実行結果です。

Tests run: 12, Failures: 0, Errors: 0, Skipped: 0

この記事とあわせて読みたい

関連記事Spring Bootの入力値はどこで受ける?@RequestParam・@PathVariable・@RequestBodyの使い分け Spring Bootの入力値は、URLの一部なら @PathVariable 、クエリ文字列なら @RequestParam 、JSONボディなら @RequestBody 、HTMLフォームならFormクラスで受けるのが基本です。値がどこから来るかを見ると迷いにくくなります。 記事を読む 関連記事Spring Bootで入力チェックが重複する|ControllerとServiceの分け方 先に結論画面とAPIの両方に同じ重複チェックを書いているなら、メール重複などの業務判断をServiceへ集約します。 記事を読む 関連記事Spring BootのControllerでやってはいけないこと Spring Bootの Controller は、リクエストを受け取り、入力を確認し、 Service を呼び出し、レスポンスを返す入口です。業務ロジック、DBアクセス、長い条件分岐、Entityの直接返却を詰め込むと、レビューで指摘されやすくなります。 記事を読む

まとめ

@RequestBodyは、JSONなどのリクエストボディをJavaオブジェクトとして受け取るためのアノテーションです。

400になったら、まず本文が送られたか、次に文法と型、最後にDTOの制約を確かめます。HttpMessageNotReadableException と MethodArgumentNotValidException を区別すると、送信データのどこを直すべきか絞れます。

-実装・エラー解決
-,