Scala用のBeanValidationを作った話(Optionとか対応してます)


こんにちは@s_tsukaです。

今日はJavaにあるBeanValidationをscala用に作成しました、という話です。ライブラリは以下のURLに公開しています。Maven Centralにあがっていますので普通に使えるかと思います。

bean-validation-scala/bean-validation-scala - GitHub

BeanValidationについてご存知の方は冒頭の説明を飛ばしてください。

BeanValidationとは(Javaの例)

みなさんはJavaのBeanValidationをご存知でしょうか。JSR303とかJSR349という仕様のアレです。

簡単に言ってしまうとJavaのPOJO、つまり普通のentityオブジェクトを検証するための仕組みです。

ライブラリをビルドパスに加えて、例えば以下のようにアノテーションを付けて、とあるメソッドにオブジェクトを与えると検証してくれる、という感じです。

public class Person {
  @Min(0)
  private int age;
  
  @NotNull
  @Size(min = 1, max = 30)
  private String name;

  // getter, setter
}

検証結果はエラーの一覧(エラーメッセージのようなもののSet)として提供されます。

上記のアノテーションは何を意味しているかというと、年齢は0以上である必要がある。名前は1文字以上30文字以内である必要がある、ということを表しています。

より詳細な使い方については以下のURLが参考になるかと思います。

JSR 303 Bean Validationで遊んでみるよ! - Yamkazu’s Blog

Javaユーザの方でBeanValidationを使いたい人はhibernateがライブラリを公開しているので、これを使いましょう。

Hibernate Validator - Hibernate Validator

独自のアノテーションを作りたい場合は前に記事を書いていますので、よろしければ参考にしてください。

Bean Validationで独自のアノーテションを作成し、検証を行う - つかびーの技術日記

BeanValidationはScalaでは微妙に使えない(Scalaの例)

さて、便利なBeanValidationですが、scalaの場合は微妙に使えません。もちろん先ほどのPersonクラスのようなcase classを作ってそれをhibernateのvalidatorで検証する場合は正常に動作します。ですが、scala独自の型では正しく動きません。例えば・・・

case class Person(
  @(Min@field)(0)
  age: Option[Int],

  @(Size@field)(min = 1, max = 30)
  name: Option[String]
)

このようなケース。これは型がOptionになっています。

これをhibernateのbean-validationで実行すると以下のようなエラーになります。

HV000030: No validator could be found for type: scala.Option<java.lang.String>. javax.validation.UnexpectedTypeException: HV000030: No validator could be found for type: scala.Option<java.lang.String>. at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintVali datorManager.verifyResolveWasUnique(ConstraintValidatorManager.java: 218)

Option[String]様のvalidatorが見つからないと言っていますね。そりゃそうだ。hibernateはJava用なのでscalaのOption型に対応したvalidatorを用意しているとは思えません。

今回これをなんとかしよう!と思い作ったライブラリがbean-validation-scalaです。

bean-validation-scala

bean-validation-scala/bean-validation-scala - GitHub

使い方はReadmeに書いてありますので、おそらくリンク先をみればわかると思います。ですが、一応日本語でも解説しておきます。

まずはsbtに依存性を記述します。versionは上記のサイトで最新のものを確認してください。

libraryDependencies ++= Seq(
  "com.tsukaby" %% "bean-validation-scala" % "0.3.0"
)

次に実際に使っていきます。例えばこんな感じで

import javax.validation.constraints.Size

import com.tsukaby.bean_validation_scala.ScalaValidatorFactory

import scala.annotation.meta.field

case class Person(
                   @(Size@field)(min = 1, max = 30)
                   name: Option[String]
                   )

object Main {
  def main(args: Array[String]): Unit = {
    val validator = ScalaValidatorFactory.validator

    val invalidBean1 = Person(Some(""))

    val violations = validator.validate(invalidBean1)
    violations.foreach(x => println(x.getMessage))

    val invalidBean2 = Person(Some("1234567890123456789012345678901"))
    val violations2 = validator.validate(invalidBean2)
    violations2.foreach(x => println(x.getMessage))


    // 出力
    // size must be between 1 and 30
    // size must be between 1 and 30
  }
}

ScalaValidatorFactoryでvalidatorを作ります。このvalidatorを使ってvalidate関数を呼び出し、beanを食わせます。

すると、違反があればエラーのSetを返す、という感じです。

上記の場合は1個目のbeanも2個目のbeanもサイズ違反(1文字以上30文字以内ではない)のため、Set内にエラーが入った状態になる、という感じです。

ほとんどhibernateのBeanValidationと同じ、というかそのラッパーライブラリなのですが、OptionやSeqにも対応しているので、普通に使えるかと思います。

今後の方針としては便利なアノテーションを増やしたり、他のScalaのクラスに対応したりなどを考えています。

みなさん、よかったら使ってみてください!

PRやご意見やGithub starもお待ちしております。

bean-validation-scala/bean-validation-scala - GitHub