spray-jsonのススメ

scala で Json を取り扱うときの筆頭ライブラリ `spray-json` の使い方メモ。
[spray/spray-json](https://github.com/spray/spray-json)

## tl;dr
- Json キーとクラスフィールドをそのままに変換する場合には `jsonFormatN`
- Json キーとクラスフィールドを名前を変えて変換する場合には `josnFormat`
- Json キーとクラスフィールドの変換組み合わせを独自で定義したい場合には `RootJsonFormat[T]`
  - `RootJsonFormat[T]`を継承した object を定義して、`read`, `write`メソッドをオーバーライドする

## 全体像
公式 `spray-json` リポジトリにとても分かりやすい図がある。

![Conversions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139404/c0fef44e-66f1-55c8-e347-7950e1c73eb3.png)
画像引用：[spray/spray-jsonのREADMEより](https://github.com/spray/spray-json)

この図で表していることは、

- `String.parseJson`を呼ぶと、JsValue が得られる
- `JsValue.convertTo[T]`を呼ぶと、ScalaType T型(独自定義のClass等)が得られる
- `ScalaType.toJson`を呼ぶと、JsValue が得られる
- `JsValue.compactPrint`を呼ぶと、String が得られる

である。
各型に変換用のメソッドがあるので、該当するメソッドを呼べばいいだけである。

ただし、上記の図のクラス `Person` ように
JsValue <-> 独自定義クラス を相互変換する場合、
どのようにして変換されるのか、というのを決定するのに `Format` が必要である。
それを下記で解説する。

## 使い方
Personクラス <-> JsValue
の変換をコードで見てみる。

全体像の項で触れたように、独自クラスと JsValue 間の変換には `Format` が必要で、
`jsonFormatN` を使って Format を定義している。

```scala
  case class Person(name: String, age: Int)
  implicit val personJsFmt = jsonFormat2(Person.apply)

  val person = Person("Ed", 24)

  // case class -> JsValue
  val personJson = person.toJson // -> {"age":24,"name":"Ed"}

  // JsValue -> case class
  val convertToPerson = personJson.convertTo[Person] // -> Person(Ed,24)
```

`jsonFormat2`の `2` というは、Person クラスのフィールドの数に対応している。
Format は `toJson` や `convertTo` メソッドの暗黙のパラメーターとして渡されるので、同一のスコープに定義する必要がある。

※ IntelliJIDEA を使っている場合、`toJson`や`convertTo`にカーソルをあわせて `cmd + shift + P` を入力すると、
暗黙的に渡している引数の定義(ここでは `personJsFmt` )にジャンプすることができる。

### Json キー名とクラスフィールドの変数名を変える

`Jsonキーはスネークケースだけど、クラスに変換したらキャメルケースで扱いたい`
というのは往々にしてある。その場合、以下のような `Format` を定義する。

```scala
  case class Person(name: String, age: Int)
  implicit val personJsFmt = jsonFormat(Person.apply, "name_string", "age_int")
```

今度は `jsonFormat` メソッドを使い、第2引数以降に `Person` クラスのフィールドに対応する Json キー名を書いていく。


```scala
  val person = Person("Ed", 24)

  val personJson = person.toJson // -> {"age_int":24,"name_string":"Ed"}

  val convertToPerson = personJson.convertTo[Person] // -> Person(Ed,24)
```

`jsonFormat`を使う場合、第1引数で渡したクラスのフィールドの数の分第2引数以降`String`を指定する必要があり、省略はできない。


### Json キーにはないフィールドを変換先クラスに追加する

基本的に Json キーとクラスフィールドは1対1でそのまま変換したほうがわかりやすい。
だが、時には Json の複数キーの値を加工してクラスの1フィールドに当てる、みたいなこともしたい。
例を示す。

```scala
  val targetJson =
    """
      |{
      |  photo_views: 10,
      |  video_plays: 20,
      |  others: 30
      |}
      |""".stripMargin
```

上記のような Json を想定し、以下の `Clicks` というクラスに変換したい。

```scala
  case class Clicks(photoViews: Int, videoPlays: Int, others: Int, totalCount: Int)
```

`totalCaount` という Json にはないフィールドを持っている。
このようにクラスの生成を独自にカスタマイズしたい場合は `RootJsonFormat[T]` を使って Format を定義する。

```scala
  trait JsonProtocol extends DefaultJsonProtocol {
    implicit object clicksJsFmt extends RootJsonFormat[Clicks] {
      override def write(obj: Clicks): JsValue = {}
      override def read(json: JsValue): Clicks = {}
    }
  }
```

`RootJsonFormat[T]`をextendsしたobjectを作り、`write`, `read`メソッドを実装する。

write -> ClicksからJsValueに変換するFormat
read -> JsValueからClicksに変換するFormat

`read`では、JsValueから特定のキーの値を型付きで取り出す、という処理が必要になる。
そこで下記のような共通部品を定義しておく。

```scala
  object JsonUtil {
    def int(v: JsValue, key: String): Option[Int] = v match {
      case JsObject(m) => m.get(key).flatMap(asInt)
      case _ => None
    }
    def asInt(v: JsValue): Option[Int] = v match {
      case JsNumber(n) if Int.MinValue <= n && n <= Int.MaxValue => Some(n.toInt)
      case _ => None
    }

    def str(v: JsValue, key: String): Option[String] = v match {
      case JsObject(m) => m.get(key).flatMap(asStr)
    }

    def asStr(v: JsValue): Option[String] = v match {
      case JsString(s) => Some(s)
      case _ => None
    }
  }
```

共通部品を使って `RootJsonFormat[Clicks]` を実装する。

```scala
  trait ClicksJsonProtocol extends DefaultJsonProtocol {
    implicit object clicksJsFmt extends RootJsonFormat[Clicks] {
      override def write(obj: Clicks): JsValue = Map(
        "photo_views" -> obj.photoViews.toJson,
        "video_plays" -> obj.videoPlays.toJson,
        "others" -> obj.others.toJson
      ).toJson

      override def read(json: JsValue): Clicks = {
        val photoViews = JsonUtil.int(json, "photo_views").getOrElse(0)
        val videoPlays = JsonUtil.int(json, "video_plays").getOrElse(0)
        val others = JsonUtil.int(json, "others").getOrElse(0)
        Clicks(
          photoViews = photoViews,
          videoPlays = videoPlays,
          others = others,
          totalCount = photoViews + videoPlays + others
        )
      }
    }
  }
```

上記のFormatを定義して、`ClicksJsonProtocol`をextendsして同一スコープに置くと目的の変換が達成できる。

```scala
  val clicks = Clicks(10, 20, 30, 60)

  val clicksJsValue = clicks.toJson // -> {"photo_views":10,"video_plays":20,"others":30}

  val convertToClicks = clicksJsValue.convertTo[Clicks]  // -> Clicks(10,20,30,60)
```

また、`String(Json doc)`の`Json.parseJson`からでも変換できることを確認してみる。

```scala
  val clicksJsValue = targetJson.parseJson // -> {"others":30,"photo_views":10,"video_plays":20}

  val clicks = clicksJsValue.convertTo[Clicks] // -> Clicks(10,20,30,60)

  val clicksToJson = clicks.toJson  // -> {"photo_views":10,"video_plays":20,"others":30}
```

## 複雑なJsonに対応する

### ケーススタディ１

- Json キー値に JsonObject や配列がある

```scala
  val targetJson =
    """
      |{
      |  "name": "Ed",
      |  "age": 24,
      |  "code": [99, 100],
      |  "metadata": {
      |    "email": "Ed@example.com",
      |    "tel": "08012345678"
      |  }
      |}
      |""".stripMargin
```

上記のようなJsonを想定する。

この場合、`targetJson`に相当する `case class`と `metadata` に相当する `case class` を作り、
入れ子にすることで変換ができる。

```scala
  case class Person(
    name: String,
    age: Int,
    code: Seq[Int],
    metadata: MetaData
  )

  case class MetaData(
    email: String,
    tel: String
  )
```

そしてそれぞれの `case class` のFormatを定義する。

```scala
  implicit val metadataJsFmt = jsonFormat2(MetaData.apply)
  implicit val personJsFmt = jsonFormat4(Person.apply)
```

このとき、最下層 object である `MetaData` に対応するFormatから先に定義しなくてはならない。
`personJsFmt` には `Person` のパラメーターである `MetaData` のFormatも必要になるからである。

```scala
  val personJsValue = targetJson.parseJson
  // -> {"age":24,"code":[99,100],"metadata":{"email":"Ed@example.com","tel":"08012345678"},"name":"Ed"}

  val person = personJsValue.convertTo[Person]
  // -> Person(Ed,24,List(99, 100),MetaData(Ed@example.com,08012345678))

  val personToJson = person.toJson
  // -> {"age":24,"code":[99,100],"metadata":{"email":"Ed@example.com","tel":"08012345678"},"name":"Ed"}
```

各変換ができていることを確認できる。

※ `person.code` が `List(99,100)` となっているが、実際に `person.code` をコード内使用すると `Seq[Int]` 型である。

### ケーススタディ２
ほとんどないパターンであると思われるが、私が某SNSGraphApiを叩いたときに遭遇したパターン。
解決方法として記載する。

- Json キー値に配列で jsonObject をもつ
 - 配列内の jsonObject は`values`を持つが、その型は同一ではない　

```scala
  val targetJson =
    """
      |{
      |  "name": "Ed",
      |  "age": 24,
      |  "metadata": [
      |    {
      |      "title": "rank",
      |      "values": 10
      |    },
      |    {
      |      "title": "job",
      |      "values": "saber"
      |    }
      |  ]
      |}
      |""".stripMargin
```

`metadata` には jsonObject の配列があり、`title` と `values` を持っているが
`values` は `Int` と `String` どちらかである。

このような場合、`values` は抽象で定義してしまおう。

```scala
  case class Person(
    name: String,
    age: Int,
    metadata: Seq[MetaData]
  )

  case class MetaData(
    title: String,
    values: MetaDataValues
  )

  sealed trait MetaDataValues
  case class MetaDataString(values: String) extends MetaDataValues
  case class MetaDataInt(values: Int) extends MetaDataValues
```

そして、各 `case class` に Format を定義する。


```scala
  trait PersonJsFmt extends DefaultJsonProtocol {
    implicit val metadataStringJsFmt = jsonFormat1(MetaDataString.apply)
    implicit val metadataIntJsFmt = jsonFormat1(MetaDataInt.apply)

    implicit object metadataJsFmt extends RootJsonFormat[MetaData] {
      override def write(obj: MetaData): JsValue = Map(
        "title" -> obj.title.toJson,
        "values" -> (obj.values match {
          case MetaDataString(v) => v.toJson
          case MetaDataInt(v) => v.toJson
        })
      ).toJson

      override def read(json: JsValue): MetaData = {
        val title = JsonUtil.str(json, "title").getOrElse("")
        MetaData(
          title = title,
          values = title match {
            case "rank" => MetaDataInt(JsonUtil.int(json, "values").getOrElse(0))
            case "job" => MetaDataString(JsonUtil.str(json, "values").getOrElse(""))
          }
        )
      }
    }

    implicit val personJsFmt = jsonFormat3(Person.apply)
  }
```

ケーススタディ１と同様、変換クラスの最下層から順に定義していく。

`MetaData` のFormatは少々複雑だが、やっていることは
`values = String` なのか `values = Int` なのかで処理を振り分けている。
このような振り分けをするために `RootJsonFormat[T]` を使っている。

JsValue からMedaData に変換する `read`メソッドの場合、
Values が `String` なのか `Int` なのかをは title で判断している。


```scala
  val personJsValue = targetJson.parseJson
  // -> {"age":24,"metadata":[{"title":"rank","values":10},{"title":"job","values":"saber"}],"name":"Ed"}

  val person = personJsValue.convertTo[Person]
  // -> Person(Ed,24,List(MetaData(rank,MetaDataInt(10)), MetaData(job,MetaDataString(saber))))

  val personToJson = person.toJson
  // -> {"age":24,"metadata":[{"title":"rank","values":10},{"title":"job","values":"saber"}],"name":"Ed"}
```

無事、相互変換ができている。


## 参考
https://github.com/spray/spray-json
https://qiita.com/petitviolet/items/79f2bd3b4f1d54d38db1