Scala
Akka
Akka-HTTP
ScalaDay 2

Akka HTTPのバリデーションライブラリを作った

課題

  • Akka HTTPを普通のAPIサーバーとして使う際に、リクエストのJSONが適切かどうかのバリデーションを掛けたい。
  • 値のバリデーションはドメインロジックでやっても良いのだけど、そもそもマイナス値が入らないようにしたいとか、入力必須項目が埋まってないとかを返したい。

という事がありました。PlayだとFormにバリデーションのAPIがあるのですが、Akka HTTPでは無さそう&調べても3rd partyライブラリとしてもあんまりなさそう。

ということで、試しに自分で作ってみました。

作ったもの

https://github.com/uryyyyyyy/akka-http-json-validation

バリデーションに違反しているリクエストを送ると、このようなレスポンスが返ってきます。

スクリーンショット 2018-12-01 11.29.53.png

ネストしたオブジェクトやArrayにも対応しています。

レスポンスのdetailsのところには、送られたjsonの「どのKeyがどういう理由でダメなのか」をそれぞれ返してくれます。

作るにあたって考えたこと

API仕様

PlayのValidationだと、返り値が

{
  "obj1.obj2.column1[0]": "error.required"
}

のような形で返ってくるので、エラー内容を画面で扱うにはKeyの文字列をパースしないといけなくて使いにくかったので、今回はこのような形で返したかった。

{
  "obj1": {
    "obj2": {
       "column1": {
         "0": "error.required"
       }
    }
  }
}

コードの解説

大きさとしては100行もない小さなライブラリです。

Validation定義

JSONは、Object, Arrayがネストしており、最後にリテラル値が出てきます。
それらを扱うため、

trait ValidatorBase[T] {
  def validate(model: T): Option[JsValue]
}
trait ValidatorSeq[T] extends ValidatorBase[Seq[T]]
trait Validator[T] extends ValidatorBase[T]

をそれぞれ定義します。
そして、それぞれで再帰的にvalidateメソッドを呼ぶことでバリデーション結果が組み立てられ、最終的には一つのOption[JsValue]型が生成され、それをレスポンスに渡す形になります。

実際にユーザーのコードでバリデーションを定義するときには、
- どのKeyのバリデーションか
- どんなバリデーションを掛けるか
- 失敗した場合にどんなメッセージを出すか
といった内容を渡すことで定義します。

case class Tag(
  id: Int,
  text: String
)

object TagValidator extends Validator[Tag] {
  private def idRule: Validation =
    genValidation(
      "id",
      tag => tag.id <= 0,
      "id must be positive"
    )

  private def textRule: Validation =
    genValidation(
      "text",
      tag => tag.text.isEmpty,
      "text must not be empty"
    )

  val validations = Seq(idRule, textRule)
}

また、ネストしたオブジェクトに対してのバリデーションをしたければ、ネストした先のオブジェクトに対して、既に作ったValidatorを渡すことで対応します。

private def tagRule: Validation =
    genValidationInternal(
      "tag",
      nested => nested.tag,
      TagValidator
    )

Validation呼び出し

Akka HTTPのJSON Supportとやっていることは同じで、
- 自作クラスの変換(バリデーション)を定義して、呼び出し側でextendsする
- 変換(バリデーション)のAPIを呼ぶときに、implicit parameterとして上記で作った定義を渡す。

という形です。
https://doc.akka.io/docs/akka-http/current/common/json-support.html

trait CustomValidationDirectives extends ValidationDirectiveBase {
  implicit val tagV = TagValidator
  implicit val taglistV = TagListValidator
  implicit val nestedTagV = NestedTagValidator
}


object Main extends CustomValidationDirectives with CustomJsonFormat {

  validate(as[Tag]) { tag => // JSON to validated Tag
   ...
  }

ValidationDirectiveBaseをextendsした自前のカスタムディレクティブを作ります。
呼び出し側では、それを継承することで、validate ディレクティブと、implicitにValidatorを渡すas[T]が使えるようになります。

ここで、as[T]はakka-http-spray-jsonのas[T]ではない事に注意です。

// akka-http-spray-json
def as[T](implicit um: FromRequestUnmarshaller[T]) = um

// akka-http-json-validation
def as[T](implicit um: FromRequestUnmarshaller[T], validator: ValidatorBase[T]): (FromRequestUnmarshaller[T], ValidatorBase[T]) = (um, validator)

validateディレクティブでは、この「JSONからEntityへの変換定義」、「Entityのバリデーション定義」を使うことによって、validate済みのデータを返してくれます。

まとめ

実装を通じて、Akka HTTPでのカスタムディレクティブの使い方や、implicitに注入されるものの振る舞いがわかりました。

短いコードなので、ぜひ読んでいただいてコメントいただけると嬉しいです。