LoginSignup
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-01

課題

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

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

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

作ったもの

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

スクリーンショット 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に注入されるものの振る舞いがわかりました。

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

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
1