ScalaのJSONライブラリcirceの使い方

ScalaJSONライブラリと言うと、json4sあたりが有名なのかと思います(私感)。

が、json4sはリフレクションを使うので、何かと避けたい方も多いかと思う。

ということで、circeを使ってみましょう。

circe概要

circeはScalaJSONライブラリです。

読み方

公式サイトに書かれています。

たしかにサーシーと読めるが、circeで調べるとWikipediaにヒットするので、キルケー呼びを推したい所存。通じれば何でもいいと思う。

特徴

元々はArgonautをforkして作られたものなので、それに似た特徴を持つ。

  • 純粋関数型のライブラリ(リフレクションを使っていない)
  • catsを内部的に使用
  • shepelessによるJSONオブジェクト ⇔ データ型の相互変換
  • Scala.js対応
  • Argonautにある複雑なオペレーター(--\とか)はすべて削除されている

各モジュールの用途

circeはモジュールに分かれている。よく使うモジュールはcirce-core, circe-parser, circe-genericの3つ。

circe-core

コアなクラス(Json, Encoder, Decoder, etc…)が含まれている。よく使うパッケージはio.circeとio.circe.syntaxのふたつ。

circe-parser

文字列をパースしてJSONオブジェクトに変換してくれるやつ。パーサーはjawnを使用していて、実態はcirce-jawnモジュールにある。

io.circe.parserパッケージのみがあり、parse関数やdecode関数を提供している。

circe-generic

よく使うパッケージはio.circe.generic.autoパッケージ。これを取り込んでおくと、JSONオブジェクト ⇔ case classおよび主要な型(String, Int, etc…)の相互変換を可能にしてくれる。

このモジュールによる変換は、変換が可能な場合コンパイルを通してくれる優れもの。反対に言うと、コンパイルが通らない場合は変換できない型が含まれている。その場合はその型のEncoder/Decoderを定義しておけばちゃんとcirce-genericモジュールで変換が可能になる。

よってコンパイルエラーとなった場合、ちゃんとすべての型が変換できるか確認すると良さ気。

その他モジュール

circe-opticsモジュールはJSONオブジェクトの操作(深い階層にある値を書き換えたりできる)を行えたりと、その他いろいろある。が、今回は割愛。

エンコード

JSONライブラリの主要な使い道はJSON文字列からデータ型への変換と、データ型からJSON文字列の変換を行うという2つだと思う。よってその2つの変換の方法を紹介。

データ型 → JSONオブジェクトの変換はエンコードと呼び、asJsonメソッドで行う。

asJsonメソッドの戻り値はio.circe.Jsonのオブジェクトとなり、このオブジェクトのnoSpacesメソッドやspaces4メソッドなどで文字列に変換できるので、HTTPのレスポンスなどとして返す場合はそのメソッドを用いる。

import io.circe.syntax._       // これがないと`asJson`メソッドが使えない
import io.circe.generic.auto._ // これがないと`io.circe.Encoder[Person]`を自動定義してくれなくて、`asJson`メソッドに渡すEncoderがないと怒られる

case class Person(name: String)

val person = Person("takkkun")

val json = person.asJson

println(json.noSpaces) // => {"name":"takkkun"}

asJsonメソッドはio.circe.Encoder[A]をimplicit parameterで受け取るようになっている。circe-genericモジュールのとこでも書いたように、case classと主要な型であればio.circe.generic.autoパッケージを取り込むだけでそれらの型のEncoderが使えるようになるので、上記のコードはちゃんと動作する。

もし定義されていない型のEncoderが必要になる場合は、自前で定義する。

import io.circe._
import io.circe.syntax._
import io.circe.generic.auto._

import org.joda.time.LocalDate

case class Person(name: String, birthday: LocalDate)

val person = Person("takkkun", new LocalDate(1987, 1, 15))

// `io.circe.Encoder[org.joda.time.LocalDate]`が定義されていないのでコンパイルエラー
person.asJson

implicit val localDateEncoder = new Encoder[LocalDate] {
  final def apply(a: LocalDate): Json =
    Json.fromString(a.toString("yyyy-MM-dd"))
}

val json = person.asJson

println(json.noSpaces) // => {"name":"takkkun","birthday":"1987-01-15"}

Encoderの定義方法いろいろ。以下はすべてio.circe.Encoder[org.joda.time.LocalDate]を定義している。

// `io.circe.Encoder[A]`トレイトを継承してその場で定義
implicit val localDateEncoder =
  new Encoder[LocalDate] {
    final def apply(a: LocalDate): Json =
      Json.fromString(a.toString("yyyy-MM-dd"))
  }

// `io.circe.Encoder[A].contramap`を使用(Aの箇所は変換先のJSON型を指定)
implicit val localDateEncoder: Encoder[LocalDate] =
  Encoder[String].contramap(_.toString("yyyy-MM-dd"))

// `io.circe.Encoder.encodeString.contramap`を使用(Stringの箇所は変換先のJSON型を指定、encodeIntとか)
implicit val localDateEncoder: Encoder[LocalDate] =
  Encoder.encodeString.contramap(_.toString("yyyy-MM-dd"))

デコード

JSONオブジェクト → データ型の変換はデコードと呼び、parse関数およびio.circe.Jsonまたはio.circe.HCursor (io.circe.ACursor)のasメソッドで行う。JSONオブジェクトにせず、JSON文字列から直接データ型へ変換する場合はdecode関数を使う。

import io.circe.parser._       // これがないと`parse`関数および`decode`関数が使えない
import io.circe.generic.auto._ // これがないと`io.circe.Decoder[Person]`を自動定義してくれなくて、`as`メソッドに渡すDecoderがないと怒られる

case class Person(name: String)

val jsonString = """{"name": "takkkun"}"""

parse(jsonString).right.flatMap(_.as[Person]) match {
  case Right(person) => println(person) // => Person(takkkun)
  case Left(error)   => println(error)
}

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

asメソッドおよびdecode関数はio.circe.Decoder[A]をimplicit parameterで受け取るようになっている。エンコードのときと同様、case classと主要な型であればio.circe.generic.autoパッケージを取り込むだけでそれらの型のDecoderが使えるようになるので、上記のコードはちゃんと動作する。

もし定義されていない型のDecoderが必要になる場合は、自前で定義する。

import io.circe._
import io.circe.parser._
import io.circe.generic.auto._

import org.joda.time.LocalDate

case class Person(name: String, birthday: LocalDate)

val jsonString = """{"name": "takkkun", "birthday": "1987-01-15"}"""

// `io.circe.Decoder[org.joda.time.LocalDate]`が定義されていないのでコンパイルエラー
decode[Person](jsonString)

implicit val localDateDecoder = new Decoder[LocalDate] {
  final def apply(c: HCursor): Decoder.Result[LocalDate] =
    c.as[String].right.map(LocalDate.parse)
}

decode[Person](jsonString) match {
  case Right(person) => println(person) // => Person(takkkun,1987-01-15)
  case Left(error)   => println(error)
}

Decoderの定義方法いろいろ。以下はすべてio.circe.Decoder[org.joda.time.LocalDate]を定義している。

// `io.circe.Decoder[A]`トレイトを継承してその場で定義
implicit val localDateDecoder =
  new Decoder[LocalDate] {
    final def apply(c: HCursor): Decoder.Result[LocalDate] =
      c.as[String].right.map(LocalDate.parse)
  }

// `io.circe.Decoder[A].map`を使用(Aの箇所は変換元のJSON型を指定)
implicit val localDateDecoder: Decoder[LocalDate] =
  Decoder[String].map(LocalDate.parse)

// `io.circe.Decoder.decodeString.map`を使用(Stringの箇所は変換元のJSON型を指定、decodeIntとか)
implicit val localDateDecoder: Decoder[LocalDate] =
  Decoder.decodeString.map(LocalDate.parse)

まとめ

circeによる相互変換はだいたい書いたやつで事足りる。と思う。少なくとも私が実際にjson4sからcirceに乗り換えたときには。

Argonautも使ってみたが、毎回すべての型に対してEncoder/Decoderを書かなければいけないので、なかなか面倒だった。その点circeは必要な分だけ書けばいいので、これが思いの外嬉しい。実際に書いたらよく分かった。

その上でリフレクション使っていないので、なんというか安心感があって助かる。今後はcirceを使っていこうかな。