ScalaのJSONライブラリと言うと、json4sあたりが有名なのかと思います(私感)。
が、json4sはリフレクションを使うので、何かと避けたい方も多いかと思う。
ということで、circeを使ってみましょう。
circe概要
circeはScalaのJSONライブラリです。
読み方
公式サイトに書かれています。
- SUR-see = サーシー(英語)
- KEER-kee = キルケー(古代ギリシャ語)
- CHEER-chay = は?(教会ラテン語)
たしかにサーシーと読めるが、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._
import io.circe.generic.auto._
case class Person(name: String)
val person = Person("takkkun")
val json = person.asJson
println(json.noSpaces)
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))
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)
Encoderの定義方法いろいろ。以下はすべてio.circe.Encoder[org.joda.time.LocalDate]
を定義している。
implicit val localDateEncoder =
new Encoder[LocalDate] {
final def apply(a: LocalDate): Json =
Json.fromString(a.toString("yyyy-MM-dd"))
}
implicit val localDateEncoder: Encoder[LocalDate] =
Encoder[String].contramap(_.toString("yyyy-MM-dd"))
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._
import io.circe.generic.auto._
case class Person(name: String)
val jsonString = """{"name": "takkkun"}"""
parse(jsonString).right.flatMap(_.as[Person]) match {
case Right(person) => println(person)
case Left(error) => println(error)
}
decode[Person](jsonString) match {
case Right(person) => println(person)
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"}"""
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)
case Left(error) => println(error)
}
Decoderの定義方法いろいろ。以下はすべてio.circe.Decoder[org.joda.time.LocalDate]
を定義している。
implicit val localDateDecoder =
new Decoder[LocalDate] {
final def apply(c: HCursor): Decoder.Result[LocalDate] =
c.as[String].right.map(LocalDate.parse)
}
implicit val localDateDecoder: Decoder[LocalDate] =
Decoder[String].map(LocalDate.parse)
implicit val localDateDecoder: Decoder[LocalDate] =
Decoder.decodeString.map(LocalDate.parse)
まとめ
circeによる相互変換はだいたい書いたやつで事足りる。と思う。少なくとも私が実際にjson4sからcirceに乗り換えたときには。
Argonautも使ってみたが、毎回すべての型に対してEncoder/Decoderを書かなければいけないので、なかなか面倒だった。その点circeは必要な分だけ書けばいいので、これが思いの外嬉しい。実際に書いたらよく分かった。
その上でリフレクション使っていないので、なんというか安心感があって助かる。今後はcirceを使っていこうかな。