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

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

```http
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
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を定義します。

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

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

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

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

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

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


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

```scala
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になります。

```scala
trait YourAppService extends HttpService with JsonRejectionHandler {
  ...
}
```

[directive]:http://spray.io/documentation/1.2.3/spray-routing/key-concepts/directives/
[reject]: http://spray.io/documentation/1.2.3/spray-routing/key-concepts/rejections/
[RejectionHandler]: https://github.com/spray/spray/blob/release/1.2/spray-routing/src/main/scala/spray/routing/RejectionHandler.scala
[予め定義されたRejection]: https://github.com/spray/spray/blob/release/1.2/spray-routing/src/main/scala/spray/routing/Rejection.scala