Help us understand the problem. What is going on with this article?

Kotlin + Spring Bootでリクエスト本文のバリデーションが効かない場合の対処

普段KotlinでSpring Bootアプリケーションを書いています。

で、 @NotBlank@Size(max = 10) などでバリデーションを指定できると様々な記事で書かれているわけですが、これがいつも効かなくて困っていました。やっとこさ解決できたので、その方法をまとめておきます。

サンプルの仕様

以下のような仕様とします。

  • Personというクラスがあり、nameを持つ
  • nameはPersonName型として指定する(値オブジェクト的な)
  • nameは空文字列を拒否し、10文字以下とする
  • Personの登録を行うエンドポイントを持ち、JSONで受け取る

サンプルの仕様にもとづいたコード

Personクラスの定義や、PersonName用のJsonDeserializerの定義は省略しています。

PersonName.kt

import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size

data class PersonName(
  @NotBlank
  @Size(max = 10)
  val value: String
)

PersonRegisterRequestBody.kt

class PersonRegisterRequestBody(
  val name: PersonName
)

PersonController.kt

import javax.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.validation.BindingResult
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class PersonController {

  @PostMapping("/register")
  fun register(
    @RequestBody @Valid requestBody: PersonRegisterRequestBody,
    result: BindingResult
  ): ResponseEntity<*> {
    if (result.hasErrors()) {
      throw // ...
    }
    // ...
  }
}

「サンプルの仕様にもとづいたコード」の問題点

大体こんな感じのコードだと思います。ポイントとしては:

  • バリデーション対象にBean Validationの @NotBlank アノテーションや @Size アノテーションを付与し、バリデーションのルールを定義
  • 受け取るリクエスト本文に @Valid アノテーション(あるいは @Validated アノテーション)を付与
  • BindingResultでバリデーション結果を受け取る

というところでしょうか。

しかし、実際にこのSpring Bootアプリケーションを起動し、対象のエンドポイントに {"name": ""} なJSONをぶん投げると、 result.hasErrors() はfalseになります。パッと見 @NotBlank アノテーションが効いていないように見えます。

解決法

いくつか問題があるので、ひとつずつ潰していきます。

そもそもバリデーションを行っていない

バリデーションのルールはたしかに定義されているのですが、これだけではバリデーションが実行されません。そりゃBindingResultも常にエラーなしになるわ。

以下の依存をbuild.gradleに追加しましょう(別のビルドツール使っている方はよしなに)。

implementation "org.springframework.boot:spring-boot-starter-validation:$springBootVersion"

正直これが一番ハマったかもしれない。

ちなみにこの依存を追加していなくても、使える人は使える模様。Spring Bootの仕様を読む限り、JSR-303実装のバリデーター(Hibernateバリデーターなど)がクラスパス上にあれば自動的にバリデーションが有効になるっぽい?実際、spring-boot-starter-validationもhibernate-validatorを依存に追加しているだけ

アノテーションの対象が間違っている

Spring Bootの仕様だと思いますが、バリデーションのルールは基本フィールドに対して付与する必要があります。

しかし、Kotlinのclassのコンストラクターにvalを指定した場合、それはコンストラクターのパラメーターでもあるし、フィールドでもあるし、ゲッターでもあります。なので単にアノテーションを付与すると、開発者が期待する対象と実際の対象が食い違うことが起こり得ます。実際、今回の @NotBlank などのアノテーションはコンストラクターのパラメーターに対して付与されちゃいます。

KotlinにはAnnotation User-site Targetsという仕組みがあり、何に対するアノテーションなのか明示的に記述できます。今回はフィールドに対するアノテーションなので、以下のようにしましょう。

import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size

data class PersonName(
  @field: NotBlank
  @field: Size(max = 10)
  val value: String
)

いずれのアノテーションも同一の対象なので、まとめて以下のように書いてもOKです。

import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size

data class PersonName(
  @field: [NotBlank Size(max = 10)]
  val value: String
)

値オブジェクトのバリデーションが実行されない

サンプルの仕様にもとづいたコードのように、リクエスト本文で受け取る型をPersonNameのような値オブジェクトめいたものにし、そちらのバリデーションのルールを流用しちゃおう、みたいなやつは、先述したコードでは動作しません。

なぜなら「PersonRegisterRequestBodyが受け取るPersonNameは有効であるべき」という指定が抜けているからです。結論から言うと以下のようにします。フィールドを対象にすることを忘れずに!

import javax.validation.Valid

class PersonRegisterRequestBody(
  @field: Valid
  val name: PersonName
)

なるほど、たしかにコントローラーのメソッドの引数には @Valid アノテーションを付与し、「有効であるべき」という指定をしているんだから、それをクラスのフィールドに対しても行わないといかんよな、という感じで納得できる(感想)。

まあStringなどを直接受け取るのであれば別に頭悩ませることではないと思いますが!

サンプルの仕様にもとづいた正しいコード

というわけで解決法を適用した正しいコードは下記の通り。これでめでたし。

build.gradle

dependencies {
  // ...

  implementation "org.springframework.boot:spring-boot-starter-validation:$springBootVersion"

  // ...
}

PersonName.kt

import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size

data class PersonName(
  @field: NotBlank
  @field: Size(max = 10)
  val value: String
)

PersonRegisterRequestBody.kt

import javax.validation.Valid

class PersonRegisterRequestBody(
  @field: Valid
  val name: PersonName
)

PersonController.kt

変更なし。

welmo
介護福祉の課題をテクノロジーで解決するサービスの開発と、子どもの可能性を解放する障がい児支援施設を運営しています。
https://welmo.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした