最近よくやるPlayFrameworkでのRequest Body(Json)の簡易Validation

業務では BodyParser での parse 時に 簡易的なValidationをすることが多い。以前は、型チェック程度の本当に簡易な Validation しかしていなかったが、最近はもう少し細かく Validation をしつつ、レスポンスを分けれるようにする書き方をすることが多いので紹介する。

1 基本となるValidator を準備する

大元になる validator をまずは準備する。

import play.api.libs.json.{Json, Reads}
import play.api.mvc.Results.BadRequest
import play.api.mvc.{BodyParser, PlayBodyParsers, Result}

import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success, Try}

trait JsonValidator {

  def validateJson[A: Reads](
      fail: Throwable => Result
  )(implicit ec: ExecutionContext, parser: PlayBodyParsers): BodyParser[A] = {
    parser.json.validate { jsValue =>
      Try(jsValue.validate[A]) match {
        case Success(value) =>
          value.asEither.left.map { e =>
            BadRequest(Json.toJson("parser error"))
          }
        case Failure(exception) => Left(fail(exception))
      }
    }
  }
}

レスポンスはよしなに。

Throwable を引数にとって、Result を返す関数 fail をリクエストごとに実装する感じで使う。

2 リクエストモデルを作成する

リクエストモデルを作成する。リクエストモデルには、リクエストの仕様を記述する。ここでは、新規ユーザー追加のリクエストモデルを作ってみることにする。仕様として、パスワードは8文字以上とする。ユーザーリクエストのプライマリコンストラクタで引数の値をチェックし、(必要に応じて)独自例外(もしくは普通の例外)を投げる。この例外を先程の Failure でキャッチし、fail でレスポンスを分ける。

case class AddUserRequest(
    firstName: String,
    lastName: String,
    password: String
) {
  if (password.length < 8) throw new IllegalPasswordError
}

object AddUserRequest {
  implicit def jsonFormat: OFormat[AddUserRequest] = Json.format[AddUserRequest]
}

class IllegalPasswordError extends Throwable

こうしておくことで、新規ユーザーリクエストのモデルを見れば、このリクエストが受け付ける値がクラスを見ただけで分かるようになる。

3 リクエスト用のvalidatorを作成し、例外ごとに処理を分ける

最後に、例外に応じて、処理を分けるようにする。

リクエスト用の trait を作成する。

trait AddUserRequestValidator extends JsonValidator {
  implicit def parse: PlayBodyParsers

  def addUserRequestValidate(implicit ec: ExecutionContext): BodyParser[AddUserRequest] = validateJson[AddUserRequest] {
    case _: IllegalPasswordError => BadRequest("パスワードは8文字以上")
    case NonFatal(e)             => InternalServerError(e.toString)
  }
}

最初に作った、JsonValidator を継承した trait を作成し、モデルが投げる例外ごとにレスポンスを分ける。パターンマッチによる振り分けで記述できるので可読性高くレスポンスを分けられる(と思う)。

まとめ

雑ではあるが、簡易的な Json の validation を紹介した。例外を使うことで、リクエストモデルに仕様を記述しつつ、validation をすることが可能になっている。問題点としては、例外を使っているため、返ってくる例外を知らないとレスポンスを分けられないことだ。これに関してはリクエストモデルと、そのモデルに対応するvalidator trait を近く(私は同一ファイルに書いてしまう)に書くことで対処できるかなと考えている。また、ここでは json のvalidationのみを扱っているが、JsonValidator を書き換えることで、他の形式にも対応可能になると思う。

値の制約程度の簡易Validationはリクエストモデルで十分だが、DBにデータが存在するかなど、通信が入るvalidationにはこのままだと使えない。そこは諦めて、Sevice層などで対応することにしている。