ScalaのJSONライブラリcirceの使い方
ScalaのJSONライブラリと言うと、json4sあたりが有名なのかと思います(私感)。
が、json4sはリフレクションを使うので、何かと避けたい方も多いかと思う。
ということで、circeを使ってみましょう。
circe概要
読み方
公式サイトに書かれています。
たしかにサーシーと読めるが、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を使っていこうかな。