8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ScalaのJSONライブラリcirceの基本

Last updated at Posted at 2020-07-24

はじめに

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

parseEitherを返すので当然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に変換するための書き方を中心に書けたら書きます

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?