【Kotlin】バリデーションの書き方の基礎【SpringBoot】

この記事は[MicroAd Advent Calendar](https://qiita.com/advent-calendar/2018/microad)の5日目の記事です。

## TL;DR
Kotlinでバリデーションをやります。主に書くのは以下2点についてです。

1. アノテーションを使ったバリデーション
2. AssertTrueを使ったバリデーション

他の書き方もありますが、とりあえず自分が知っているのがこの2つなのでまとめます。
当初アノテーション自作まで書いてましたが、長くなりすぎたので分割し、7日目に回しました。

- [【Kotlin】アノテーションを自作する【SpringBoot】 \- Qiita](https://qiita.com/wrongwrong/items/8abf3b1b09654da3cb36)

## 前書き
この記事は以下の記事を元に書いています。

- [【Kotlin】SpringBootでGetのコントローラーを動かすまで【SpringBoot】 \- wrongwrongな開発日記](https://wrongwrong163377.hatenablog.com/entry/2018/12/02/145113)
- [【Kotlin】SpringBootでControllerが受け取った内容をバリデーションするまで【SpringBoot】 \- wrongwrongな開発日記](https://wrongwrong163377.hatenablog.com/entry/2018/12/03/003000)

### コントローラー
コントローラーは以下を用います。
受け取ったmyModelに対してバリデーションを行い、引っかかればエラーを、引っかからなければpostした内容を返しています。

```kotlin:MyController.kt
import com.wrongwrong.modeltest.model.MyModel
import org.springframework.validation.BindingResult
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("my")
class MyController{

    @PostMapping
    fun myPostTest(
            @RequestBody @Validated myModel: MyModel,
            bindingResult: BindingResult
    ): String {
        //エラーがあればエラーを文字列にして返す
        if(bindingResult.hasErrors()){
            return bindingResult.allErrors.toString()
        }
        //受け取った内容を返却
        return "post:" + myModel.toString()
    }
}
```
エラーの無い状態で叩くと以下のようになります。

```bash:エラー無し
$ curl -X POST -H "Content-Type: application/json" -d '{"id":1, "name":"wrong wrong", "create":"2018-11-01", "update":"2018-11-02"}' localhost:8080/my
post:MyModel(id=1, name=wrong wrong, create=Thu Nov 01 09:00:00 JST 2018, update=Fri Nov 02 09:00:00 JST 2018)
```

### モデル
今回バリデーションを行う上で、元にするモデルは以下です。
全てNullableとしなければ正常にバリデーションが行えないので、必ずNullableで宣言してください（説明は本題と外れるので省略します）。
バリデーションしたい内容はコメントの通りです。

```kotlin:MyModel.kt
import java.util.*

data class MyModel(
        //　nullを禁止したい
        val id: Long?,
        // 名前がスペース区切りになっていることを確認したい
        val name: String?,
        // createはupdateと同じか過去にしたい
        val create: Date?,
        val update: Date?
)
```

## アノテーションを使ったバリデーション
まずidのバリデーションを、アノテーションとして実装されたバリデーターを使ってやっていきます。
data classでは、`@field:NotNull`というようにアノテーションを付与します。
Javaでは`field:`無しでもバリデーションが行えますが、Data Classではこれを付けなければ正常にバリデーションができません。
アノテーションを付与すると以下のようになります。

```kotlin:MyModel.kt
import java.util.*
import javax.validation.constraints.NotNull

data class MyModel(
        @field:NotNull(message = "idはnull不許可")
        val id: Long?,
        // 名前がスペース区切りになっていることを確認したい
        val name: String?,
        // createはupdateと同じか過去にしたい
        val create: Date?,
        val update: Date?
)
```
この状態でエラー有り/無しをそれぞれCurlで叩いた結果が以下です。

```bash:エラー無し
$ curl -X POST -H "Content-Type: application/json" -d '{"id":1}' localhost:8080/my
post:MyModel(id=1, name=null, create=null, update=null)
```
```bash:エラー有り
$ curl -X POST -H "Content-Type: application/json" -d '{}' localhost:8080/my
[Field error in object 'myModel' on field 'id': rejected value [null]; codes [NotNull.myModel.id,NotNull.id,NotNull.java.lang.Long,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [myModel.id,id]; arguments []; default message [id]]; default message [idはnull不許可]]
```
### 利用できるアノテーション
`javax.validation.constraints`クラスや、`org.hibernate.validator.constraints`クラスを見ておくと、既存で利用できるアノテーションは大体まとまっています。
[javax\.validation\.constraints \(Java\(TM\) EE 8 Specification APIs\)](https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html)
[org\.hibernate\.validator\.constraints \(Hibernate Validator 5\.1\.3\.Final\)](https://docs.jboss.org/hibernate/validator/5.1/api/org/hibernate/validator/constraints/package-summary.html)

## AssertTrueを使ったバリデーション
次にcreateとupdateの相関チェックをやっていきます。
相関チェックはJavaと同様、`@AssertTrue`（または`@AssertFalse`）を付与したBool型の関数で行います。
今回は相関チェックにのみ利用していますが、アノテーションが用意されていないような単体チェックにも利用できます。
相関チェックを加えると以下のようになります。

```kotlin:MyModel.kt
import java.util.*
import javax.validation.constraints.AssertTrue
import javax.validation.constraints.NotNull

data class MyModel(
        @field:NotNull(message = "idはnull不許可")
        val id: Long?,
        // 名前がスペース区切りになっていることを確認したい
        val name: String?,
        val create: Date?,
        val update: Date?
) {
    @AssertTrue(message = "updateがcreateより過去")
    fun isLater(): Boolean {
        if(create == null || update == null) return true
        return create.before(update) || create == update
    }
}
```
この状態でエラー有り/無しをそれぞれCurlで叩いた結果が以下です。

```bash:エラー有り
curl -X POST -H "Content-Type: application/json" -d '{"id":1, "create":"2018-11-03", "update":"2018-11-02"}' localhost:8080/my
[Field error in object 'myModel' on field 'later': rejected value [false]; codes [AssertTrue.myModel.later,AssertTrue.later,AssertTrue.boolean,AssertTrue]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [myModel.later,later]; arguments []; default message [later]]; default message [updateがcreateより過去]]
```

## 後書き
記事は7日目へ続きます。

## 参考にさせていただいた記事
- [curlコマンドでPOSTする \- Qiita](https://qiita.com/sensuikan1973/items/b2085a9cdc6d1e97e8f8)
- [【Kotlin】KotlinでJava EEのBean Validationを使うときの注意点 \- B\-Teck\!](http://beatdjam.hatenablog.com/entry/2018/08/06/190000)