課題
- Akka HTTPを普通のAPIサーバーとして使う際に、リクエストのJSONが適切かどうかのバリデーションを掛けたい。
- 値のバリデーションはドメインロジックでやっても良いのだけど、そもそもマイナス値が入らないようにしたいとか、入力必須項目が埋まってないとかを返したい。
という事がありました。PlayだとFormにバリデーションのAPIがあるのですが、Akka HTTPでは無さそう&調べても3rd partyライブラリとしてもあんまりなさそう。
ということで、試しに自分で作ってみました。
作ったもの
バリデーションに違反しているリクエストを送ると、このようなレスポンスが返ってきます。

ネストしたオブジェクトや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に注入されるものの振る舞いがわかりました。
短いコードなので、ぜひ読んでいただいてコメントいただけると嬉しいです。