[Akka-HTTP]spray-jsonの使い方とAkka-HTTP


Akka-HTTPで公式にサポートされているJSONライブラリの[spray-json](https://github.com/spray/spray-json)の使い方。
[JSON Support — Akka Documentation](http://doc.akka.io/docs/akka/2.4.4/scala/http/common/json-support.html)
Akka-HTTPと一緒に使いたいので、Marshaller/Unmarshallerとして使用法も載せる。

# tl;dr

- クラスのフィールドに独自の型を使わない場合 or 独自の型に対応させてjsonを入れ子にする場合
  - `jsonFormatN`を使えば手軽で良い
- クラスのフィールドとjsonのフォーマットを変えたい場合
  - `RootJsonProtocol`を一生懸命実装する
- Akka-HTTPでは`SprayJsonSupport`をextendsすればimplicitにやってくれる
	- `akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport`

# 基本的な使い方

case classをjsonに変換、再変換を行う。

```scala
case class User(id: Long, name: String)
```

このcase classに対応する`RootJsonFormat`を用意すればよい。
`DefaultJsonProtocol`をextendsしたobject内で実装するとこうなる。

```scala
object UserJsonProtocol extends DefaultJsonProtocol {
  implicit val format = jsonFormat2(User.apply)
}
```

これを使ってjson <-> case classをするとこんな感じ。

```scala
import UserJsonProtocol._

// case class -> json
val user = User(100, "alice")
user.toJson  // {"id":100,"name":"alice"}

// json -> case class
val json = "{\"id\":10,\"name\":\"bob\"}"
json.parseJson.convertTo[User]  // User(10,bob)
```

ここで使った`jsonFormat2`は`User`クラスのコンストラクタ引数が2つであることに対応している
`jsonFormatN`を使用するとフィールド名(コンストラクタの仮引数名)がjsonのkeyとなる。
フィールド名とjsonのkeyを変えたい場合は`jsonFormat`を使う。

```scala
implicit val format: RootJsonFormat[User] = jsonFormat(User.apply, "user_id", "user_name")
```    

こう書くとコンストラクタに対して順番に`user_id`, `user_name`を割り当てることとなる。
つまり、この様にjson <-> case classの関係が変化する。

```scala
// case class -> json
val user = User(100, "alice")
user.toJson  // {"user_id":100,"user_name":"alice"}

// json -> case class
val json = "{\"user_id\":10,\"user_name\":\"bob\"}"
json.parseJson.convertTo[User]  // User(10,bob)
```

## akka-httpで使う

akka-httpとspray-jsonを組み合わせる。
具体的には`ToResponseMarshaller`としてspray-jsonを使うこととなる。
`complete`にクラス`T`のオブジェクトを渡してjsonとして返却したい場合、 implicitな`RootJsonProtocol[T]`をスコープ内に用意しておけば良い。
変換がcase classからjsonへの一方向のみの場合、`RootJsonWriter`だけでも可。
Marshalling/Unmarshallingとjson<->case classの対応関係は以下。

- json -> case classがUnmarshalling
  - `RootJsonReader`が必要
- case class -> jsonがMarshalling
  - `RootJsonWriter`が必要


controller的なところに`SprayJsonSupport`をextendsしておけばimplicitにやってくれる。

```scala
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport

// jsonで返したい型
case class User(id: Long, name: String)

// implicitなRootJsonProtocol[User]を用意
implicit val format: RootJsonProtocol[User] = jsonFormat2(User.apply)

val route =
  // GET /user/<name>
  path("user" / ".+".r) { name =>
    val id: Long = new Random().nextInt(100).toLong
    val user: User = User(id, name)

    complete(user)
  }
```

リクエストとレスポンスはこのようになる。
`Content-Type: application/json`となっており、bodyもjsonになっている。

```
$ curl localhost:8080/user/alice -i
HTTP/1.1 200 OK
Server: akka-http/2.4.2
Date: Tue, 03 May 2016 10:30:40 GMT
Content-Type: application/json
Content-Length: 33

{
  "id": 90,
  "name": "alice"
}
```

# やや複雑な例

前述の例ではクラスのフィールドの型がいわゆるプリミティブ型だけのものであった。
次は複雑なオブジェクト、クラスのフィールドの型が独自クラスになっているような場合を考える。

## クラス定義

例として使うクラス定義を以下に示す。

```scala
// IDにまつわる型
trait Identifier[+A] extends Any {
  def value: A
}
object Identifier {
  type IdType = String
}
case class ID[A <: Entity[_]](value: IdType) extends AnyVal with Identifier[IdType]

// Userの親となるEntity
trait Entity[Id <: Identifier[_]] {
  val id: Id
}

// Userのフィールド型
case class Name(value: String) extends AnyVal
case class Email(value: String) extends AnyVal

case class User(id: ID[User], name: Name, email: Email) extends Entity[ID[User]]
```

## 単純なRootJsonProtocolを実装

定義した`User`クラスをjsonに変換するための`RootJsonProtocol`を実装する。
`RootJsonProtocol[User]`を実装するためには、各フィールドの型に応じた`RootJsonProtocol`が必要となる。

```scala
object UserJsonProtocol extends DefaultJsonProtocol {
  implicit val userIdFormat = jsonFormat1(ID.apply[User])
  implicit val nameFormat = jsonFormat1(Name.apply)
  implicit val emailFormat = jsonFormat1(Email.apply)
  implicit val userFormat = jsonFormat3(User.apply)
}
```

この`RootJsonProtocol`群を使って`User`クラスのオブジェクトをjsonに変換してみる。

```scala
import UserJsonProtocol._
val user = User(ID("alice-id"), Name("alice"), Email("alice@example.com"))
user.toJson
```

結果として以下の様なjsonが得られる。

```json
{
  "id": {
    "value": "alice-id"
  },
  "name": {
    "value": "alice"
  },
  "email": {
    "value": "alice@example.com"
  }
}
```

入れ子になってしまっているが、オブジェクトをクラス定義に沿って正しくserializeしていると言える。

### akka-httpでの使用

上記の`jsonFormatN`を使った`RootJsonFormat`の実装だと、なぜかakka-httpではうまくいかず以下の様なエラーメッセージが出力されてレスポンスは返ってこない。

> Caused by: java.lang.RuntimeException: Cannot automatically determine case class field names and order for 'net.petitviolet.domain.user.Name', please use the 'jsonFormat' overload with explicit field name specification

メッセージにある通り、明示的に`jsonFormat`を使う。

```scala
object UserJsonProtocol extends DefaultJsonProtocol {
  implicit val userIdFormat = jsonFormat(ID.apply[User] _, "value")
  implicit val nameFormat = jsonFormat(Name.apply _, "value")
  implicit val emailFormat = jsonFormat(Email.apply _, "value")
  implicit val userFormat = jsonFormat(User.apply, "id", "name", "email")
}
```

これで上と同様なjsonが返ってくるようになる。

## フラットなjsonを作る

入れ子になったクラスをserializeすると入れ子になったjsonが返ってきた。
入れ子になった`value`が必要でない場合、`jsonFormatN`を使った実装では実現できない。
そのため、`RootJsonProtocol[User]`を自前で実装することとなる。

```scala
object UserJsonProtocol extends DefaultJsonProtocol {
  implicit val userFormat: RootJsonFormat[User] = new RootJsonFormat[User] {
    override def read(json: JsValue): User =
      json.asJsObject.getFields("id", "name", "email") match {
        case Seq(JsString(id), JsString(name), JsString(email)) =>
          User(ID(id), Name(name), Email(email))
        case _ => throw new DeserializationException("User")
      }

    override def write(user: User): JsValue = JsObject(
      "id" -> JsString(user.id.value),
      "name" -> JsString(user.name.value),
      "email" -> JsString(user.email.value)
    )
  }
}
```  

これを使って`User`をjsonに変換するとフラットなjsonが得られる。

```json
{
  "id": "alice-id",
  "name": "alice",
  "email": "alice@example.com"
}
```