😺

Circeを使い倒して極限までJSON加工を楽しよう!

2022/01/20に公開約12,300字

背景

現在チーム開発でJSON加工するときに、ScalaのCirceライブラリを使用しています。とても強力なライブラリなので、JSON加工を楽にできますが、ライブラリ(Scala自体)の挙動を理解していないと完全に楽ができないので、レビューをしていて勿体ないなあと思うシーンがあったのとメンバーの意見としてもCirceの使用に不安があるようだったので、記事に起こしました。

この記事ではCirceの基礎・基本の使い方から徐々にステップアップして、最終的には複雑なJSON加工でも楽できるように心がけています。Circeのバージョンは、0.14.1を使用しております。

Circe使用例

直接書くケース(楽度: 低)

楽ではないやり方ですが、基本なので紹介をします。Json.objにタプルを渡していくことでJSONのオブジェクトを生成することができます。valueに当たる部分もJson型である必要があるため、asJsonを呼び出しています。このメソッドは今後の鍵となります。

楽ではないポイントとしては、このようにインラインで複数箇所に書いてしまうと(テストのassertなどを想定しています)、いざJSONの形が変わってしまうと変更が大変になってしまったり、keyのスペルミスなどが拾えないことが想定されます。単純に.asJsonと書きまくるのも大変なところですね。

import io.circe.Json
import io.circe.syntax._

object Main extends App {
  val json =
    Json.obj(
      "foo" -> 123.asJson,
      "bar" -> "hoge".asJson,
      "hoge" -> true.asJson,
      "list" -> Seq(1, 2, 3).asJson
    )
  println(json)
  // {
  //  "foo" : 123,
  //  "bar" : "hoge",
  //  "hoge" : true,
  //  "list" : [
  //    1,
  //    2,
  //    3
  //  ]
  //}
}

case classで型を利用してJSONを書くケース(楽度: 中~高)

先程のインラインにJSONを書くのではなく、case classを定義して、そのインスタンスをJSONに変換することで楽をしたいです。そうすることで、変更にも強くなりますし、keyのスペルミスなども防げます。

ということで、さっそく以下のように書きますが、このままだとエラーが発生します。

import io.circe.syntax._

case class BeJson(foo: Int, bar: String, hoge: Boolean, list: Seq[Int])

object Main extends App {
  val beJson1 =
    BeJson(foo = 123, bar = "hoge", hoge = true, list = Seq(1, 2, 3))
  val beJson2 =
    BeJson(foo = 456, bar = "aaaa", hoge = false, list = Seq(4, 5, 6))

  println(beJson1.asJson)
  println(beJson2.asJson)
}

起きるエラーは以下の通り。BeJsonクラスのEncoderが無いですよ!と言われます。

Main.scala:13:19: could not find implicit value for parameter encoder: io.circe.Encoder[example.BeJson]
[error]   println(beJson1.asJson)

circeのコードを抜粋すると、以下のようにasJsonは、implicitでEncoder[A](上記のコードでのAは、BeJsonとなる)を求めていることがわかります。最初の例などに書いた、Int, Long, Bool, Seqなどのプリミティブな値に対するEncoder, DecoderはCirceが定義しているというわけですね(例えば、Encoderの定義はこちら)。

final def asJson(implicit encoder: Encoder[A]): Json = encoder(value)

Encoderを用意してみる(楽度: 中)

BeJsonのコンパニオンオブジェクトとして、encoderをimplicit valとして定義してあげると、先程のコードが通るようになります。

import io.circe.{Encoder, Json}
import io.circe.syntax._

case class BeJson(foo: Int, bar: String, hoge: Boolean, list: Seq[Int])

object BeJson {
  implicit val encoder: Encoder[BeJson] = Encoder.instance(
    beJson =>
      Json.obj(
        "foo" -> beJson.foo.asJson,
        "bar" -> beJson.bar.asJson,
        "hoge" -> beJson.hoge.asJson,
        "list" -> beJson.list.asJson
    )
  )
}

object Main extends App {
  import BeJson._ // コンパニオンオブジェクトにすると、自動的に読み込まれるため、これがなくても動きます。

  val beJson1 =
    BeJson(foo = 123, bar = "hoge", hoge = true, list = Seq(1, 2, 3))
  val beJson2 =
    BeJson(foo = 456, bar = "aaaa", hoge = false, list = Seq(4, 5, 6))

  println(beJson1.asJson)
  println(beJson2.asJson)
  //{
  //  "foo" : 123,
  //  "bar" : "hoge",
  //  "hoge" : true,
  //  "list" : [
  //    1,
  //    2,
  //    3
  //  ]
  //}
  //{
  //  "foo" : 456,
  //  "bar" : "aaaa",
  //  "hoge" : false,
  //  "list" : [
  //    4,
  //    5,
  //    6
  //  ]
  //}
  
}

Encoderを導出してみる(楽度: 中)

プリミティブな型について、Encoderが用意されているなら、もっと楽ができるはずです!io.circe.generic.semiauto.deriveEncoderを使って、Encoderを半自動でderiving(導出)します。これは楽ですね!

import io.circe.Encoder
import io.circe.syntax._

case class BeJson(foo: Int, bar: String, hoge: Boolean, list: Seq[Int])

object BeJson {
  implicit val encoder: Encoder[BeJson] =
    // Instances for case classes, "incomplete" case classes, sealed trait hierarchies, etc.
    io.circe.generic.semiauto.deriveEncoder
}

object Main extends App {
  import BeJson._ // implicit val encoderをimport

  val beJson1 =
    BeJson(foo = 123, bar = "hoge", hoge = true, list = Seq(1, 2, 3))
  val beJson2 =
    BeJson(foo = 456, bar = "aaaa", hoge = false, list = Seq(4, 5, 6))

  println(beJson1.asJson)
  println(beJson2.asJson)
}

Encoderを完全に導出してみる(楽度: 高)

半自動で導出ということは完全に自動導出もできるはずです!io.circe.generic.auto._を使ってみましょう!これは、Scalaのマクロ機能が利用されています。興味がある方は是非調べてみてください。

import io.circe.syntax._

case class BeJson(foo: Int, bar: String, hoge: Boolean, list: Seq[Int])

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

  val beJson1 =
    BeJson(foo = 123, bar = "hoge", hoge = true, list = Seq(1, 2, 3))
  val beJson2 =
    BeJson(foo = 456, bar = "aaaa", hoge = false, list = Seq(4, 5, 6))

  println(beJson1.asJson)
  println(beJson2.asJson)
}

ネストしたcase classでも導出したい(楽度: 超楽)

Jsonを加工する現場はもっと過酷です。きっとネストしたクラス(JSONオブジェクト)も出現するはずです。ということでネストしたクラスを用意してみました。

import io.circe.syntax._

case class BeJson(foo: Int, childList: Seq[BeJsonChild])

case class BeJsonChild(bar: String, hoge: Boolean)

object Main extends App {
  val beJson =
    BeJson(foo = 123, childList = Seq(BeJsonChild(bar = "hoge", hoge = true)))

  println(beJson.asJson)
}

当然Encoderを用意していないのでエラーがでます。

Main.scala:13:18: could not find implicit value for parameter encoder: io.circe.Encoder[example.BeJson]
[error]   println(beJson.asJson)

ネストしたcase classで半自動導出してみる(楽度: 高)

ネストしていてもやることは同じです。deriveEncoderを呼び出しましょう。ただし、親のderiveをするときは、子供のencoderをimportするようにしましょう。

import io.circe.Encoder
import io.circe.syntax._

case class BeJson(foo: Int, childList: Seq[BeJsonChild])

object BeJson {
  // 子供のencoderをimportしておく必要がある
  import BeJsonChild._
  implicit val encoder: Encoder[BeJson] =
    io.circe.generic.semiauto.deriveEncoder
}

case class BeJsonChild(bar: String, hoge: Boolean)

object BeJsonChild {
  implicit val encoder: Encoder[BeJsonChild] =
    io.circe.generic.semiauto.deriveEncoder
}

object Main extends App {
  import BeJson._

  val beJson =
    BeJson(foo = 123, childList = Seq(BeJsonChild(bar = "hoge", hoge = true)))

  println(beJson.asJson)
  //{
  //  "foo" : 123,
  //  "childList" : [
  //    {
  //      "bar" : "hoge",
  //      "hoge" : true
  //    }
  //  ]
  //}
}

ネストしたcase classで完全自動導出してみる(楽度: 超楽)

circe(Scala)のマクロは強力です。なんと、完全に自動導出が可能です。

import io.circe.syntax._

case class BeJson(foo: Int, childList: Seq[BeJsonChild])

case class BeJsonChild(bar: String, hoge: Boolean)

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

  val beJson =
    BeJson(foo = 123, childList = Seq(BeJsonChild(bar = "hoge", hoge = true)))

  println(beJson.asJson)
}

完全自動導出が使えないとき(楽度: 高)

多くの場合、 import io.circe.generic.auto._を使えば良いことがわかりました。しかし、それだけでは達成できない要件があることに気づきます。その場合でも適切に対処して、楽をしていきましょう。

Jsonの読み込みを学ぶ

今まではJSONへの書き出し(Encode)の例だけを見てきたので、気分転換にJSONからScalaの値に読み込む(Decode)例を見ていきましょう。読み込みは失敗する可能性があるので、Either[失敗理由, 成功の型]の型で返ってくることに注意しましょう。それ以外は、書き出しのときと変わりません。

import io.circe.{Decoder, Json}
import io.circe.syntax._

case class FromJson(foo: Int, bar: String, hoge: Boolean)

object FromJson {
  val decoder: Decoder[FromJson] = io.circe.generic.semiauto.deriveDecoder
}

object Main extends App {
  val fromJsonObject =
    Json.obj("foo" -> 123.asJson, "bar" -> "abc".asJson, "hoge" -> true.asJson)
  println(FromJson.decoder.decodeJson(fromJsonObject))
  // Right(FromJson(123,abc,true))

  val fromJsonObjectError =
    Json.obj("aaaa" -> 123.asJson, "bbb" -> "abc".asJson, "hoge" -> true.asJson)
  println(FromJson.decoder.decodeJson(fromJsonObjectError))
  // Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(foo))))
}

言語感の違いを埋めるための手動読み込み(楽度: 低)

それでは完全導出が出来ない例を見ていきましょう。JavaScriptはScalaと違い数値の桁数の精度には限界があります。(将来的には、BigIntが浸透したら必要のないテクニックになります!)ブラウザのコンソールで精度の限界を確かめてみましょう。

console.log(Number.MAX_SAFE_INTEGER);

VM38:1 9007199254740991

このような場合、JSONではStringとして受け取り、ScalaではLongに変換するなどの手間が必要です。

import io.circe.{Decoder, Json}
import io.circe.syntax._

case class FromJson(foo: Long, bar: String, hoge: Boolean)

object FromJson {
  val decoder: Decoder[FromJson] = Decoder.instance { c =>
    for {
      foo <- c.downField("foo").as[String]
      bar <- c.downField("bar").as[String]
      hoge <- c.downField("hoge").as[Boolean]

    } yield FromJson(foo.toLong, bar, hoge)
  }
}

object Main extends App {
  val fromJsonObject =
    Json.obj(
      "foo" -> "123456789000001234".asJson,
      "bar" -> "abc".asJson,
      "hoge" -> true.asJson
    )
  println(FromJson.decoder.decodeJson(fromJsonObject))
  // Right(FromJson(123456789000001234,abc,true))
}

独自の型を加工してみる(楽度: 低~高)

他の完全自動導出が出来ないケースとして、Enumをabstruct classとして定義しているようなケースを考えてみます。

import io.circe.syntax._

abstract class PublishStatus(val code: String, val value: String)

object PublishStatus {

  case object PRIVATE extends PublishStatus("PRIVATE", "非公開")

  case object LIMITED extends PublishStatus("LIMITED", "限定公開")

  case object PUBLIC extends PublishStatus("PUBLIC", "公開")

  def fromCode(code: String): PublishStatus =
    code match {
      case "PRIVATE" => PublishStatus.PRIVATE
      case "LIMITED" => PublishStatus.LIMITED
      case "PUBLIC"  => PublishStatus.PUBLIC
      case _ =>
        throw new IllegalArgumentException(s"invalid PublishStatus: $code")
    }
}

case class BeJson(foo: Int, publishStatus: PublishStatus)

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

  val beJson =
    BeJson(1, PublishStatus.PRIVATE)

  println(beJson.asJson)
}

これは当然、どんな型でEnumを出力して欲しいか指定をしていないためエラーを吐きます。

Main.scala:33:18: could not find implicit value for parameter encoder: io.circe.Encoder[example.BeJson]
[error]   println(beJson.asJson)

Encoderを手書きしてみる(楽度: 低)

まず愚直にエラーを治す方法として、EnumのEncoderを手書きしてみます。

import io.circe.{Encoder, Json}
import io.circe.syntax._

abstract class PublishStatus(val code: String, val value: String)

object PublishStatus {

  case object PRIVATE extends PublishStatus("PRIVATE", "非公開")

  case object LIMITED extends PublishStatus("LIMITED", "限定公開")

  case object PUBLIC extends PublishStatus("PUBLIC", "公開")

  def fromCode(code: String): PublishStatus =
    code match {
      case "PRIVATE" => PublishStatus.PRIVATE
      case "LIMITED" => PublishStatus.LIMITED
      case "PUBLIC"  => PublishStatus.PUBLIC
      case _ =>
        throw new IllegalArgumentException(s"invalid PublishStatus: $code")
    }
}

case class BeJson(foo: Int, bar: String, publishStatus: PublishStatus)

object BeJson {
  implicit val encoder: Encoder[BeJson] = Encoder.instance(
    beJson =>
      Json.obj(
        "foo" -> beJson.foo.asJson,
        "bar" -> beJson.bar.asJson,
        "publishStatus" -> beJson.publishStatus.code.asJson
    )
  )

}

object Main extends App {
  import BeJson._

  val beJson =
    BeJson(foo = 1, bar = "foo", publishStatus = PublishStatus.PRIVATE)

  println(beJson.asJson)
  // {
  //  "foo" : 1,
  //  "bar" : "foo",
  //  "publishStatus" : "PRIVATE"
  //}
}

Enumだけを手書き加工し、後は自動導出する(楽度: 高)

前回との違いは、PublishStatusのコンパニオンオブジェクトにEncoderをimplicit valで定義しておけば、あとはimport io.circe.generic.auto._で自動導出することができます。これは楽ですね!

import io.circe.Encoder
import io.circe.syntax._

abstract class PublishStatus(val code: String, val value: String)

object PublishStatus {
  implicit val encoder: Encoder[PublishStatus] =
    Encoder.instance(publishStatus => publishStatus.code.asJson)

  case object PRIVATE extends PublishStatus("PRIVATE", "非公開")

  case object LIMITED extends PublishStatus("LIMITED", "限定公開")

  case object PUBLIC extends PublishStatus("PUBLIC", "公開")

  def fromCode(code: String): PublishStatus =
    code match {
      case "PRIVATE" => PublishStatus.PRIVATE
      case "LIMITED" => PublishStatus.LIMITED
      case "PUBLIC"  => PublishStatus.PUBLIC
      case _ =>
        throw new IllegalArgumentException(s"invalid PublishStatus: $code")
    }
}

case class BeJson(foo: Int, bar: String, publishStatus: PublishStatus)

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

  val beJson =
    BeJson(foo = 1, bar = "foo", publishStatus = PublishStatus.PRIVATE)

  println(beJson.asJson)
  // {
  //  "foo" : 1,
  //  "bar" : "foo",
  //  "publishStatus" : "PRIVATE"
  //}
}

まとめ

いろんなCirceの書き方を見てきました。これを基本として抑えておけば、あとはいくらクラスがネストしても独自の型を使っていたとしても、多くの場合は楽に加工が行えるはずです! もっと賢い使い方や注意点などあればコメントにて指摘お願いします!チームのみんな!もっと楽をしていこう!!

Discussion

ログインするとコメントできます