circeで値クラス含みのクラスのエンコード/デコード

#1. 値クラスについて
Scalaには[値クラス (value class)](https://docs.scala-lang.org/ja/overviews/core/value-classes.html)と呼ばれる機構があります。
値クラスは要は単なるラッパークラスですが、ラッパーとして振る舞うのはコンパイルまでで、実行時のデータ構造には内部の値を直接使ってくれる(ラッパーオブジェクトへのメモリ割り当てが回避される)クールなやつです。
値クラスは下記のように、ただ1つのpublicな`val`パラメータを持つクラスを、`AnyVal`のサブクラスとして定義します。

``` IntWrapper.scala
class IntWrapper(val x: Int) extends AnyVal
```

値クラスのおかげで、メモリを気にせずラッパーの恩恵(型安全性、リッチなコンテクストなど)を享受できます。[^1]

#2. circeについて
[circe](https://circe.github.io/circe/)(サースィー、またはキルケーと読むらしい)は、Scala用のオープンソースのJSONライブラリです。
既存型のみならず、ユーザー定義型でもそのままいい感じにJSONと相互変換してくれるスマートなやつです。
circeの使い方自体は、公式ドキュメントや以下のサイトなどを参考にしてください。

[ScalaのJSONライブラリcirceの使い方](http://takkkun.hatenablog.com/entry/2017/04/04/Scala%E3%81%AEJSON%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AAcirce%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)
[circeを使ってJSONとScalaクラスを相互変換する](https://dev.classmethod.jp/etc/circe-first-guide/)

#3. circeで値クラス含みのクラスをJSONと相互変換する
本題です。
例として、以下のようなクラスを考えます。

```Book.scala
case class Book(id: BookId, title: String)
case class BookId(value: Long) extends AnyVal
```

`Book`クラスは`BookId`クラスの値と文字列をフィールドに持ちます。
`BookId`クラスは値クラスで、ただの`Long`のラッパーです。


この`Book`クラスを、circeでJSONと相互変換してみます。

##3-1. 依存性の追加
まずはcirceを使えるよう、`build.sbt`に依存性を追加します。
ここはさらっと流します。

```build.sbt
val circeVersion = "0.12.3"

libraryDependencies ++= Seq(
  "io.circe" %% "circe-core" % circeVersion,
  "io.circe" %% "circe-generic" % circeVersion,
  "io.circe" %% "circe-parser" % circeVersion,
)
```

##3-2. 通常変換
通常時の挙動を確かめるために、何も手を加えずにエンコードしてみます(`Book` → JSON文字列)。

```Main.scala
object Main extends App {
  import io.circe.generic.auto._
  import io.circe.syntax._

  val book = Book(BookId(1), "Scalaスケーラブルプログラミング")

  // asJsonでBookをcirce.Jsonに変換し、spaces2でcirce.JsonをStringに変換している
  println(book.asJson.spaces2)
}
```

```console:出力結果
{
  "id" : {
    "value" : 1
  },
  "title" : "Scalaスケーラブルプログラミング"
}
```

circeの力によりJSONへの変換自体は問題なくできたものの、`BookId`クラスの部分がネストしています。
おそらく上記の`value`の部分が必要なケースはほとんどなく、通常はネストを取り除いた下記のようなJSONにしたいのではないでしょうか?

```console:顧客が本当に欲しかったもの
{
  "id" : 1,
  "title" : "Scalaスケーラブルプログラミング"
}
```

また、このネストはデコード時にも問題になります(JSON文字列 → `Book`)。
現状では下記のように、`BookId`クラスの構造に合わせてネストしたJSONを渡さなければ、`Book`クラスのインスタンスを生成できません。

```Main.scala
object Main2 extends App {
  import io.circe.generic.auto._
  import io.circe.parser.decode

  // ネストなし(通常想定されるJSON)
  val json = """ { 
      | "id": 1,
      | "title": "Scalaスケーラブルプログラミング"
      | } """.stripMargin

  // ネストあり(BookIdクラスの構造を加味したJSON)
  val nestedJson = """ { 
     | "id": { 
     |   "value": 1 
     | },
     | "title": "Scalaスケーラブルプログラミング"
     | } """.stripMargin

  // ネストなしだとデコードに失敗する
  println("normal: " + decode[Book](json))
  // ネストありだと成功する
  println("nested: " + decode[Book](nestedJson))
}
```

```console:出力結果
normal: Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(value), DownField(id))))
nested: Right(Book(BookId(1),Scalaスケーラブルプログラミング))
```

<br>
これらの`BookId`クラスのネストについて、エンコード時(`Book` → JSON文字列)はネストを取り除き、デコード時(JSON文字列 → `Book`)はネストのない文字列から`Book`インスタンスを生成できるようにするのが、今回の主題です。

##3-3. ネストの排除
まず、`BookId`用の`Encoder`/`Decoder`を定義したトレイトを用意します。
`Encoder`と`Decoder`はどちらも型クラスなので、`BookId`クラスを`Encoder`および`Decoder`のインスタンスとして宣言します。
(｢型クラス｣の意味がわからなくても、なんとなく無名クラスを作っているんだなくらいの理解でOKです)

```BookIdJsonSupport.scala
import io.circe.{Decoder, Encoder, HCursor, Json}
import io.circe.syntax._

trait BookIdJsonSupport {
  // BookId → JSON
  implicit val bookIdEncoder: Encoder[BookId] = new Encoder[BookId] {
    override def apply(bookId: BookId): Json = bookId.value.asJson
  }

  // JSON → BookId
  implicit val bookIdDecoder: Decoder[BookId] = new Decoder[BookId] {
    override def apply(c: HCursor): Decoder.Result[BookId] =
      c.value.as[Long].map(BookId.apply)
  }
}
```

<b>重要な点として、それぞれ`implicit`キーワードをつけて宣言する必要があります。</b>

`Encoder`トレイトと`Decoder`トレイトは(型は違いますが)それぞれ`apply`というただ一つの抽象メソッドを持つため、それらを実装した無名クラスを定義しています。

`bookIdEncoder[BookId]`は`BookId`をJSONに変換するとき、自身(`BookId`)ではなく自身の持つ値(`Long`)をJSONに変換します。
これによりJSONから`value`という項目がなくなり、ネストが排除されます。


`bookIdDecoder`は少しわかりづらいかもしれません。
`HCursor`クラスの詳細は割愛しますが、この`c.value`がJSONの値(1とか"hoge"とか)になります。
(※この`value`は`BookId`クラスの`value`フィールドとは全く無関係の別物です。)

`bookIdDecoder`はJSONの値をまずは`Long`として解釈し、それからその`Long`値を`BookId`クラスに変換しています。

戻り値の型の`Decoder.Result[A]`は、単なる`Either[DecodingFailure, A]`の型エイリアスです。

```Decoder.scala
final type Result[A] = Either[DecodingFailure, A]
```

JSONのデコードは失敗する可能性があるため、結果を`Either`でラップしています。

細かい注意点として、上記の`Encoder[BookId]/Decoder[BookId]`を使用する際は、逆に今までのようにネスト含みのJSONを生成することも、`{ "value" : 100 }`のような`BookId`の構造通りのJSONのデコードもできなくなります。
それで困るような場面もまずないと思いますが、必要に応じて使い分けてください。

<br>
利用する際は、上記のトレイトを`Book`クラス(`BookId`クラスではない)をエンコード/デコードしたいクラスにミックスインします。

```Main.scala
import io.circe.generic.auto._
import io.circe.parser.decode
import io.circe.syntax._

object Main extends App with BookIdJsonSupport {
  val book = Book(BookId(1), "Scalaスケーラブルプログラミング")
  val json = """ { "id":2, "title": "実践Scala入門" } """
  
  // 変換時に暗黙のEncoder[BookId]/Decoder[BookId]を必要とする場合、
  // BookIdJsonSupportのものを使うようになる
  println(book.asJson.spaces2)
  println(decode[Book](json))
}
```

```console:出力結果
{
  "id" : 1,
  "title" : "Scalaスケーラブルプログラミング"
}
Right(Book(BookId(2),実践Scala入門))
```

無事、`BookId`のネスト部分がなくなり、またネストなしのJSONから`Book
`クラスのインスタンスを生成できるようになりました。
ここではトレイトをミックスインしていますが、`Encoder[BookId]/Decoder[BookId]`が暗黙のパラメータの探索範囲内に存在しさえすれば、他の方法でも構いません。

また実用上は上記のように`BookIdJsonSupport`を直接ミックスインするよりも、`BookJsonSupport`のような`Book`クラス用のトレイトを作り、そちらをミックスインした方が便利なケースが多いです。
こうすることで、下記のように`Book`クラスのエンコード/デコード時に`BookId`以外のフィールドも考慮しなければいけない場合も、利用側はその詳細を知らずに済みます。

```BookJsonSupport.scala
// 考慮するフィールドが増えても
trait BookJsonSupport
    extends BookIdJsonSupport
    with TitleJsonSupport
    with AuthorJsonSupport
    with DescriptionJsonSupport
```

```HogeImpl.scala
// 利用側はBookJsonSupportをミックスインするだけでいい(詳細は知らなくていい)
class HogeImpl extends Hoge with BookJsonSupport {
  // ･･･
}
```

##3-4. 表記の簡略化
ここからは余談です。
上記の`Encoder[BookId]/Decoder[BookId]`は説明用にかなりゴテゴテに書いたので、実際はもう少し簡略化できます。

###簡略化1. SAM変換
circeの`Encoder[A]`と`Decoder[A]`はどちらも抽象メソッドをただ一つだけ持つ型なので、SAM type(Single-Abstract-Method type)にあたります。
そのため、SAM変換により以下のように無名クラスをラムダ式に置き換えることが可能です。

```BookIdJsonSupport.scala
import io.circe.Decoder.Result
import io.circe.{Decoder, Encoder, HCursor, Json}
import io.circe.syntax._

trait BookIdJsonSupport {

  // BookId -> Jsonのラムダ式に置き換え
  // この_.valueはBookId#value
  implicit val bookIdEncoder: Encoder[BookId] = _.value.asJson

  // HCursor -> Decorder.Result[BookId]のラムダ式に置き換え
  // この_.valueはHCursor#value
  implicit val bookIdDecoder: Decoder[BookId] = _.value.as[Long].map(BookId.apply)

//  implicit val bookIdEncoder: Encoder[BookId] = new Encoder[BookId] {
//    override def apply(bookId: BookId): Json = bookId.value.asJson
//  }

//  implicit val bookIdDecoder: Decoder[BookId] = new Decoder[BookId] {
//    override def apply(c: HCursor): Decoder.Result[BookId] =
//      c.value.as[Long].map(BookId.apply)
//  }
}
```

###簡略化2. Encoder/Decoderのメソッドを使う
SAM変換はScalaの機能ですが、単純に`Encoder/Decoder`の便利なメソッドを使っての簡略化も可能です(多分これがデファクトスタンダード)。

```BookIdJsonSupport.scala
import io.circe.{Decoder, Encoder}

trait BookIdJsonSupport {
  implicit val bookIdEncoder: Encoder[BookId] = Encoder[Long].contramap(_.value)

  implicit val bookIdDecoder: Decoder[BookId] = Decoder[Long].map(BookId.apply)
}
```

実装の詳細は割愛しますが、やってることは最初の明示的に無名クラスを作ったときとほとんど同じです。
circeが裏でいい感じに無名クラスを作ってくれています。

特に理由がない限り、この書き方でいいんじゃないかなと思います。

#4. 最後に
circeを使う際、値クラスをどう扱えばいいのかで試行錯誤したのでまとめてみました。
参考になれば幸いです。

最後までお読み頂きありがとうございました｡
質問や不備についてはコメント欄か[Twitter](https://twitter.com/ka2_kamaboko)までお願いします。

[^1]: メモリが割り当てられる場合もありますが、本題ではないため割愛します。