3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring Boot + Kotlin でパス変数を data class にマッピングする

Posted at

結論

  • companion object に、String を引数に取るファクトリーメソッドを定義すれば良い
  • メソッド名は valueOf, of, from のいずれか
  • メソッドには @JvmStatic をつける

検証環境

Spring 6.1.6
Spring Boot 3.2.5
Kotlin 1.9.23

やりたいこと

構造を持ったデータクラスをパス変数として受け取りたい。

コントローラー
@RestController
class ClassroomController {
	@GetMapping("/classrooms/{id}")
	fun read(@PathVariable id: ClassroomId): Classroom {
		// 略
	}
}
メソッド引数の型:(年、組)を表すクラス
data class ClassroomId(val grade: Int, val group: String)

/classrooms/3-A に対するリクエストでは ClassroomId(3, "A") を引数で受け取るイメージです。Custom Editor などで対応可能ではあるものの面倒なので、なにか簡単な方法はないものかと調べていたところ、思いの外簡単に実現できることを知りました。

PathVariable の変換ルール

パス変数は @PathVariable を付けた引数で受け取れますが、大半の型変換は Spring がよしなにやってくれます。

引数の型に対応する Converter が存在しない場合、Spring Boot のデフォルトでは ObjectToObjectConverter で変換を行っているようです。
このクラスでは次の順に変換を試みます。

  1. 変換後のクラスに存在するファクトリーメソッド(String を引数に取るもの)
    ※メソッド名は valueOfoffrom のいずれか
  2. 変換後のクラスのコンストラクタ(String を引数に取るもの)

Kotlin の場合の注意

ファクトリーメソッドを追加すれば良いのですが、次のようにしても変換されません。

data class ClassroomId(val grade: Int, val group: String) {
	companion object {
        /**
         * [String] から [ClassroomId] を生成するファクトリーメソッド。
         *
         * たとえば "3-A" のような値を受け取る。
         */
		fun valueOf(stringValue: String): ClassroomId {
			val (grade, group) = stringValue.split("-")
			return ClassroomId(grade.toInt(), group)
		}
	}
}

companion object で定義した valueOf メソッドを Java から呼び出すには次のようになります。

ClassroomId.Companion.valueOf("3-A")

Spring は次のようなファクトリーメソッドを探すので、見つけてもらえないのですね。

ClassroomId.valueOf("3-A")

この問題に対処するには @JvmStatic を使います。valueOf メソッドにアノテーションをつければ、ClassroomId の static メソッドも生成されるので、Java からは以下どちらの方法でも扱えるようになります。

ClassroomId.valueOf("3-A")
ClassroomId.Companion.valueOf("3-A")

バリデーション

data class にマッピングできるともう一つ嬉しいことがあります。

ファクトリーメソッドで分解しておけば、data class の各プロパティに対して Bean Validation が適用できます。たとえば1年A組〜3年E組以外をエラーにするなら次のようになります。

data class ClassroomId(
    @field:Min(1)
    @field:Max(3)
    val grade: Int,

    @field:Pattern(regexp = "[A-E]")
    val group: String
) {
    companion object {
        @JvmStatic
        fun valueOf(stringValue: String): ClassroomId {
            val (grade, group) = stringValue.split("-")
            return ClassroomId(grade.toInt(), group)
        }
    }
}

参考

ソースコード 説明
DefaultConversionService.java デフォルトで利用される型変換サービス。ここで登録されている Converter はデフォルトで利用可能。
ObjectToObjectConverter.java#L184-L200 ObjectToObjectConverter の中で変換に使うファクトリーメソッドを探している部分。
DefaultConversionServiceTests.java#L800-L809 valueOf で変換されることのテスト。
3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?