Edited at
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に注入されるものの振る舞いがわかりました。

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