circeを使ってJSONとScalaクラスを相互変換する

2019.11.08

はい、どーも!CX事業本部の吉田です。

今回は、ScalaでJSONのパースなどによく使われるcirceの使い方をまとめてみました。

circeを使うことで、ScalaのクラスをJSONで出力したり、逆にJSONをパースしてScalaクラスにマッピングする、といったことが簡単にできるようになります。

クラスをJSONにする

まずは単純にScalaのクラスをJSONに出力してみます。次のようにケースクラスと、そのコンパニオンオブジェクトを準備します。

import io.circe.{Encoder}
import io.circe.generic.semiauto._

final case class Person(name: String, email: String)

object Person {
  implicit val encoder: Encoder[Person] = deriveEncoder
}

クラスをJSONに出力(ここではエンコード)するには、コンパニオンオブジェクト内でエンコーダーをimplicitで準備しておきます。

deriveEncoderio.circe.generic.semiauto にあり、これを使うことで対象のクラスをええ感じにJSONにエンコードしてくれます。

実際にJSONにするには

import io.circe.syntax._

val person = Person("John Doe", "john@example.com")
println(person.asJson)

としてやります。ここで io.circe.syntax._ をimportするのを忘れると asJson メソッドが生えないので注意です。

JSONをパースしてクラスにマッピングする

今度は逆にJSONをパースし、これをクラスにマッピングしてみます。

先程はエンコーダーをimplicitで準備しましたが、今度はデコーダーをコンパニオンオブジェクトに準備します。

object Person {
  implicit val encoder: Encoder[Person] = deriveEncoder
  implicit val decoder: Decoder[Person] = deriveDecoder  // <-これを追加
}

デコーダーでも、エンコーダーのときと同じように deriveDecoder を使って、JSONをええ感じにパースしてもらいます。

では実際にJSONをパースしてみましょう。

import io.circe.parser._

val json =
  """
    |{
    |   "name": "John Doe",
    |   "email": "john@example.com"
    |}
  """.stripMargin

decode[Person](json) match {
  case Right(person) => println(person)
  case Left(error) => println(error)
}

io.circe.parser._ にある decode を使ってJSONをパースします。decodeメソッドはEitherを返すので、これをパターンマッチして、成功時or失敗時で分けて処理できます。

エンコーダーを実装してJSONに出力する

次に、クラスをJSONに出力するときにJSONのフィールド名を変えたいとします。例えばクラス上では email となっている項目を、JSONでは mail_address にしたいときなどです。

この場合は deriveEncoder が使えないので、自前でEncoderを実装してやります。

import io.circe.Json._

object Person {
//  implicit val encoder: Encoder[Person] = deriveEncoder
  implicit val encoder: Encoder[Person] = (p: Person) => {
    obj(
      "name" -> fromString(p.name),
      "mail_address" -> fromString(p.email)
    )
  }

io.circe.Json._ にある obj を使い、引数のPersonを使ってJSONを組み立てる感じですね。

デコーダーを実装してクラスにマッピングする

先のようにエンコーダーを実装した結果、JSONは以下のように出力されます。

{
  "name" : "John Doe",
  "mail_address" : "john@example.com"
}

現状では、これをそのままパースしてPersonクラスにマッピングすることはできません。パースできるようにするためには同じくDecoderを自前で実装してやります。

コンパニオンオブジェクトに、以下のようにDecoderを実装します。

//  implicit val decoder: Decoder[Person] = deriveDecoder
implicit val decoder: Decoder[Person] = (c: HCursor) => {
  for {
    name <- c.downField("name").as[String]
    email <- c.downField("mail_address").as[String]
  } yield Person(name, email)
}

HCursor#downField でJSONの対象の値が取得できるので、これをfor内包記法で各値を取り出し、yieldでPersonクラスに設定してやります。

また、これを少し応用してやればJSONの値を結合して、その結果をクラスにマッピングするといったこともできますね。例えば以下のようなJSONがあった場合

{
  "first_name" : "John",
  "last_name": "Doe",
  "mail_address" : "john@example.com"
}

first_namelast_name を結合した結果をPersonのnameにセットしたい場合は、Decoderを

implicit val decoder: Decoder[Person] = (c: HCursor) => {
  for {
    first_name <- c.downField("first_name").as[String]
    last_name <- c.downField("last_name").as[String]
    email <- c.downField("mail_address").as[String]
  } yield Person(s"${first_name} ${last_name}", email)
}

とすることで、簡単に対応可能です。

キャメルケースとスネークケースを相互変換する

ここで少し話を戻して deriveEncoderderiveDecoder について。 この2つのエンコーダー&デコーダーは便利なのですが、Scalaクラスの項目名とJSONの項目名が一致している必要があります。

しかし実際には項目名が、Scalaクラスではキャメルケース、JSONはスネークケースといったパターンは多々あると思います。例えば以下のような感じ。

final case class Person(firstName: String, lastName: String, email: String)
{
  "first_name" : "John",
  "last_name": "Doe",
  "mail_address" : "john@example.com"
}

これをいい感じに相互変換するには circe-generic-extras を使うと便利です。

コンパニオンオブジェクトを以下のように修正します。

//import io.circe.generic.semiauto._  <- これをやめる
import io.circe.generic.extras.semiauto._
import io.circe.generic.extras.Configuration

object Person {
  implicit val encoder: Encoder[Person] = deriveEncoder
  implicit val decoder: Decoder[Person] = deriveDecoder
  implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
}

まずエンコーダーとデコーダーは deriveEncoderderiveDecoder を使うのですが、これは io.circe.generic.semiauto._ ではなく、 io.circe.generic.extras.semiauto._ にあるものを使います。これを忘れると、ただしく相互変換できません。

次に、キャメルケースとスネークケースで相互変換できるよう、上記のように Configuration をコンパニオンオブジェクトに指定しておきます。 (他に withKebabCaseMemberNames ケバブケース?!といったものがあります)

これだけで、相互変換できるようになるので、大変便利です。