18
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

普段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

変更なし。

18
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?