LoginSignup
1
1

More than 5 years have passed since last update.

SprayでエラーメッセージをJSONにしたい

Posted at

ScalaのREST APIフレームワークSprayですが、エラーメッセージはデフォルトでtext/plainになっています。

HTTP/1.1 400 Bad Request
Content-Length: 40
Content-Type: text/plain; charset=UTF-8
Date: Wed, 03 Jun 2015 05:51:49 GMT

Request entity expected but not supplied

REST APIを実装する上で、エラーメッセージはJSONにしたいことがあります。

Sprayのエラーハンドリング

Sprayは各directiveごとにリクエストを受け入れるか拒否(reject)するかの判断が加わります。拒否された場合、ルーティングからは外れ、RejectionHandlerがレスポンスの役目を担います。

デフォルトのRejectionHandlerがtext/plainのレスポンスを作っているので、これを換装することでカスタムのエラーレスポンスを作ることができます。

RejectionHandlerの自作

例えば、エラーメッセージとして次のようなJSONを返えす実装をしてみたいと思います。

HTTP/1.1 422 Unprocessable Entity
Content-Length: 140
Content-Type: application/json; charset=UTF-8
Date: Wed, 03 Jun 2015 05:56:57 GMT

{
    "code": "validation_failed",
    "errors": [
        {
            "code": "missing_field",
            "field": "user_id"
        }
    ],
    "message": "Validation failed"
}

まず、エラーメッセージ用のcase classを定義します。

case class Rejection(code: String, message: String, errors: Option[Seq[FieldRejection]])
case class FieldRejection(field: String, code: String)

次に、このcase classをJSONにシリアライズできるよう、protocolを作成します。

object RejectionJsonProtocol extends DefaultJsonProtocol with SprayJsonSupport {
  implicit val FieldRejectionFormat = jsonFormat2(FieldRejection)
  implicit val RejectionFormat = jsonFormat3(Rejection)
}

最後に、RejectionHandlerを継承して自前のハンドラを作ります。

trait JsonRejectionHandler {
  import RejectionJsonProtocol._
  implicit val jsonRejectionHandler = RejectionHandler {
    case MalformedRequestContentRejection(_, _) :: _ =>
      complete(UnprocessableEntity, Rejection("validation_failed", msg, None))
  }
}

MalformedRequestContentRejection予め定義されたRejectionの1つです。エラーをJSONにしたいRejectionのぶんだけ、ここにcaseを定義して拡張していきます。

以下が全体的なコードです。

package shouldbee.spray

import spray.httpx.SprayJsonSupport
import spray.routing._
import spray.http.StatusCodes._
import spray.routing.directives.RouteDirectives._

import spray.json.{ DeserializationException, DefaultJsonProtocol }

case class Rejection(code: String, message: String, errors: Option[Seq[FieldRejection]])
case class FieldRejection(field: String, code: String)

object RejectionJsonProtocol extends DefaultJsonProtocol with SprayJsonSupport {
  implicit val FieldRejectionFormat = jsonFormat2(FieldRejection)
  implicit val RejectionFormat = jsonFormat3(Rejection)
}

trait JsonRejectionHandler {
  import RejectionJsonProtocol._
  implicit val jsonRejectionHandler = RejectionHandler {
    case MalformedRequestContentRejection(msg, cause) :: _ =>
      cause match {
        case Some(DeserializationException(_, prev, fieldNames)) =>
          val code = prev match {
            case e: NoSuchElementException   => "missing_field"
            case e: IllegalArgumentException => "malformed"
            case e: Throwable                => e.getClass.getName
          }
          val errors: Seq[FieldRejection] = fieldNames.map(FieldRejection(_, code))
          complete(UnprocessableEntity, Rejection("validation_failed", "Validation failed", Some(errors)))
        case what @ _ =>
          complete(UnprocessableEntity, Rejection("validation_failed", msg, None))
      }
  }
}

Serviceに組み込む

最後に、自作したRejectionHandlerをServiceにmixinすることで、エラーがJSONになります。

trait YourAppService extends HttpService with JsonRejectionHandler {
  ...
}
1
1
0

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
1