wrongwrongな開発日記

しんまいさんの忘備録

【Kotlin】バリデーション用のアノテーションを自作する【SpringBoot】

↓の続きです。
wrongwrong163377.hatenablog.com
記事執筆時点でのプロジェクトリポジトリは以下。
github.com

やること

フィールド用に名前が半角スペースで2分割できるかをチェックするCustom Validatorを作ります。
深い理解ができていないので、作って動かす程度の内容です。
Custom Validatorを作る上で必要な作業は以下の3つです。

アノテーションクラスを用意する

アノテーションクラスは、Javaでいう@interfaceです。
早速ですが、完成したアノテーションクラスは以下の通りです。

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@ReportAsSingleViolation
@Constraint(validatedBy = [CanSplitBySpaceValidator::class])
annotation class CanSplitBySpace(
        val message: String = "message",
        val groups: Array<KClass<out Any>> = [],
        val payload: Array<KClass<out Payload>> = []
)
解説

付与しているアノテーション関連は大体Javaと同じです。個々の機能の説明は長くなるので省略します。
@Constraint(validatedBy = [CanSplitBySpaceValidator::class])に指定しているクラスは、次に実装するバリデーター本体です。
Javaとの相違点としては、@Target@Retention辺りがKotlin独自のクラスになっている点と、メッセージやgroups等の指定の方法が異なる点があります。
また、後述しますが、Kotlinで作成したアノテーションfield:を指定しなくても正常に動作します。

バリデーターを作る

こちらはJavaの内容とほぼ変わりません。

class CanSplitBySpaceValidator: ConstraintValidator<CanSplitBySpace, String>{
    override fun initialize(constraintAnnotation: CanSplitBySpace) {}

    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        //nullなら何もしない
        if(value == null) return true
        //スペースで2分割できなければいけない
        return value.split(" ").size == 2
    }
}

ConstraintValidatorに指定しているのは、作成したアノテーションと、アノテーションが処理する対象のクラスです。

ここまでを合わせて

作成したアノテーションとバリデーターは以下の通りです。

import javax.validation.*
import kotlin.reflect.KClass

//アノテーション
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@ReportAsSingleViolation
@Constraint(validatedBy = [CanSplitBySpaceValidator::class])
annotation class CanSplitBySpace(
        val message: String = "message",
        val groups: Array<KClass<out Any>> = [],
        val payload: Array<KClass<out Payload>> = []
)

//バリデーター本体
class CanSplitBySpaceValidator: ConstraintValidator<CanSplitBySpace, String>{
    override fun initialize(constraintAnnotation: CanSplitBySpace) {}

    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        //nullなら何もしない
        if(value == null) return true
        //スペースで2分割できなければいけない
        return value.split(" ").size == 2
    }
}

付与して使う

前述の通り、field:を付ける必要はありません(付けても問題なく動きます)。

//import com.wrongwrong.modeltest.annotation.CanSplitBySpace インポート
import java.util.*
import javax.validation.constraints.AssertTrue
import javax.validation.constraints.NotNull

data class MyModel(
        @field:NotNull(message = "idはnull不許可")
        val id: Long?,
        @CanSplitBySpace(message = "名前が半角スペースで2つに分割できない")
        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で叩くと以下のようになります。

$ curl -X POST -H "Content-Type: application/json" -d '{"id":1, "name":"wrongwrong", "create":"2018-11-01", "update":"2018-11-02"}' localhost:8080/my
[Field error in object 'myModel' on field 'name': rejected value [wrongwrong]; codes [CanSplitBySpace.myModel.name,CanSplitBySpace.name,CanSplitBySpace.java.lang.String,CanSplitBySpace]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [myModel.name,name]; arguments []; default message [name]]; default message [名前が半角スペースで2つに分割できない]]
$ 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)

参考にさせていただいた記事

discuss.kotlinlang.org
qiita.com
qiita.com