はじめに
Web APIをPlay Frameworkを使って開発しているが、Jsonの変換の実装がPlayのJson APIよりスマートに書けそうなcirceというライブラリの存在を知ったので、軽くメモを残したかった。
目的
とりあえず抑えておきたい基本的な書き方を残したいと思います。
ちょっと深ぼった内容はまた別の記事で書くつもりです。
セットアップ
公式のHomeにあるQuick startを見てコピペでOK
JSONのパース
基本は以下
-
parse
を使う - 戻り値は
Either[ParsingFailure, Json]
- パース可能であればRight、不可能であればLeftとなる
-
getOrElse
で失敗した場合の値を返せる
import io.circe._, io.circe.parser._
// パース可能
val rawJson = """
{
"foo": "bar",
"buzz": 3,
"hoge": [1, 2, 3]
}
"""
parse(rawJson)
// => val res0: Either[io.circe.ParsingFailure,io.circe.Json] =
// Right({
// "foo" : "bar",
// "baz" : 123,
// "list of stuff" : [
// 4,
// 5,
// 6
// ]
// })
// パース不可能
val badJson = "hoge"
parse(badJson)
// => val res1: Either[io.circe.ParsingFailure,io.circe.Json] = Left(io.circe.ParsingFailure: expected json value got 'hoge' (line 1, column 1))
// getOrElse
parse(badJson).getOrElse(Json.Null)
// => val res2: io.circe.Json = null
parse
はEither
を返すので当然match式ができる
parse(rawJson) match {
case Left(failure) => println("Invalid JSON")
case Right(json) => println("Yay, got some JSON")
}
// Yay, got some JSON
以上
実に簡単
値の抜き出し
- circeを使ってのJSONの値の抜き出しはcursorとか言うのを使う
まずは下準備
import cats.syntax.either._
import io.circe._, io.circe.parser._
val json: String = """
{
"id": "c730433b-082c-4984-9d66-855c243266f0",
"name": "Foo",
"counts": [1, 2, 3],
"values": {
"bar": true,
"baz": 100.001,
"qux": ["a", "b"]
}
}
"""
val doc: Json = parse(json).getOrElse(Json.Null)
ここではHcursorを使ってdoc
のJSONを操作していく
val cursor: HCursor = doc.hcursor
どうやらこのcursor
に対してどのキーにフォーカスをするのか指示していって、値を抜き出すっぽい
処理の順序のイメージとしてはカーソルがJSONのどこにフォーカスするかを指示し、フォーカスした場所に値があった場合は変換する感じ
最初にフォーカスされている位置はJSONツリーのルート
実際の値の抜き出しは以下で、返す型はDecoder.Result[T]
val baz = cursor.downField("values").downField("baz").as[Double]
// baz: Decoder.Result[Double] = Right(100.001)
// .downField("baz").as[Double]の代わりに.get[Double]("baz")で短くできる
val baz2 = cursor.downField("values").get[Double]("baz")
// baz2: Decoder.Result[Double] = Right(100.001)
// 配列の場合downArrayで最初の要素を抜き出す
val qux1 = cursor.downField("values").downField("qux").downArray.as[String]
// qux1: Decoder.Result[String] = Right("a")
// downNで指定したindexの要素を抜き出す
val qux2 = cursor.downField("values").downField("qux").downN(1).as[String]
// qux2: Decoder.Result[String] = Right("b")
// rightで次の要素を抜き出す
val qux3 = cursor.downField("values").downField("qux").downArray.right.as[String]
// qux3: Decoder.Result[String] = Right("b")
// leftで前の要素を抜き出す
val qux4 = cursor.downField("values").downField("qux").downN(1).left.as[String]
// qux4: Decoder.Result[String] = Right("a")
他にもどこにフォーカスするのかを指示するメソッドがあるので確認してみるといいかもしれない
ともあれ値の抜き出しについてもメソッドチェーンでできてスマート
値の変換
引き続きHCursor
から操作して値を変換できるっぽい
val reversedNameCursor = cursor.downField("name").withFocus(_.mapString(_.reverse))
val reversedName: Option[Json] = reversedNameCursor.top
// reversedName: Option[Json] = Some(
// JObject(
// object[id -> "c730433b-082c-4984-9d66-855c243266f0",name -> "ooF",counts -> [
// 1,
// 2,
// 3
// ],values -> {
// "bar" : true,
// "baz" : 100.001,
// "qux" : [
// "a",
// "b"
// ]
// }]
// )
// )
ちなみにtop
はフォーカスをルートへジャンプさせるやつ
変換を担ったwithForcus
のシグニチャは以下
def withFocus(f: Json => Json): ACursor
要するにJsonを変換する処理を渡せば、そのとおりに置き換えてくれる
あまり値を変換するケースに出くわしてないので使えるか今段階思いつかないけど、簡単にできそう
エンコーディングとデコーディング
この機能が強えからcirceを使っているまであるくらい楽
今回は自動でエンコードとデコードのやり方が定義されるのを確認していこうと思います
まずは普通にJSON化
import io.circe.syntax._
val intsJson = List(1, 2, 3).asJson
// intsJson: io.circe.Json = JArray(
// Vector(JNumber(JsonLong(1L)), JNumber(JsonLong(2L)), JNumber(JsonLong(3L)))
// )
intsJson.as[List[Int]]
// => res0: io.circe.Decoder.Result[List[Int]] = Right(List(1, 2, 3))
asJson
してエンコードし、デコードは使いたい型にただ変換すればいい
ここまでは普通だが、asJson
はcase classにも適用可能でこれが楽
case class User(name: UseName, age: Int, address: String)
case class UseName(firstName: String, familyName: String)
val user = User(UseName("foo", "bar"), 3, "buz")
println(user)
// {
// "name" : {
// "firstName" : "foo",
// "familyName" : "bar"
// },
// "age" : 3,
// "address" : "buz"
// }
なんとネストした奴らも一気にいい感じに変換してくれる
これで大体PlayのJson.Writeをいちいち定義する手間が省けて優勝
とはいえcase classがそのままJSONにならない場合もあるが、それは個別で定義が必要、これはしょうがない
それについてはまた今度書けたら書きます
デコードについてもなかなか強い
val userJson = """
{
"name" : {
"firstName" : "foo",
"familyName" : "bar"
},
"age" : 3,
"address" : "buz"
}
"""
val userModel = decode[User](userJson)
// userModel: Right(User(UseName(foo,bar),3,buz))
やはりcase classに対してデコード可能であればいい感じに変換してくれる
もちろんimplicitはとくにこっちでは定義してない
終わりに
今回はここまで
次は個別にJSONに変換するための書き方を中心に書けたら書きます