Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

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

変更なし。

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
6
Help us understand the problem. What are the problem?