Circeでエンコードされたオブジェクトの値を抽出するには?

Circeを使ってあるオブジェクトをエンコードした際に、正常にエンコードされたかどうか値を確認したいときがあります。

そのオブジェクトが複数のオブジェクトのリストを抱えている場合、どうやって確認すればよいのか少し詰まったので備忘録として残しておきます。

事前準備

まず、確認のために必要な前準備をします。

IntellJなどでプロジェクトを作成します。

次に、build.sbtを以下のように記載します。

name := "blog-circe"

version := "0.1"

scalaVersion := "2.13.5"

val circeVersion = "0.13.0"

libraryDependencies ++= Seq(
  "io.circe" %% "circe-core",
  "io.circe" %% "circe-generic",
  "io.circe" %% "circe-parser"
).map(_ % circeVersion)

libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.2"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.2" % "test"

あとは、確認に必要なクラスを作成します。

package vo

case class Tag(name: String)
case class Item(name: String, price: Int, tags: Option[Seq[Tag]] = None)
case class Cart(items: Seq[Item], totalPrice: Int)

object CartFactory {
  def create(items: Seq[Item]): Cart =
    Cart(items = items, totalPrice = items.map(_.price).sum)
}

確認方法

以下のように、テストコードを書きます。

import org.scalatest.flatspec.AnyFlatSpec
import vo.{CartFactory, Item, Tag}
import io.circe.syntax._
import io.circe.generic.auto._
import org.scalatest.matchers.should.Matchers

class CartSpec extends AnyFlatSpec with Matchers {
  it should "エンコードしたItemの名前を抽出できる" in {
    val chocolate = Item("チョコレート", 250, Some(Seq(Tag("food"))))
    val apple = Item("りんご", 100, Some(Seq(Tag("food"), Tag("fruit"))))
    val error = Item(name = "error", price = 0)
    val cart = CartFactory.create(Seq(chocolate, apple))

    val itemsCursor = cart.asJson.hcursor.downField("items")

    itemsCursor.downArray
      .as[Item]
      .getOrElse(error)
      .name shouldBe chocolate.name

    itemsCursor
      .downN(0)
      .as[Item]
      .getOrElse(error)
      .name shouldBe chocolate.name

    itemsCursor
      .downN(1)
      .as[Item]
      .getOrElse(error)
      .name shouldBe apple.name
  }
}

Circeには、データを抽出のためにCursorというものが用意されています。 これを使うことで、エンコードされたオブジェクトを読むことができます。

ちなみに、cartのjson構造はこのようになっています。

{
  "items" : [
    {
      "name" : "チョコレート",
      "price" : 250,
      "tags" : [
        {
          "name" : "food"
        }
      ]
    },
    {
      "name" : "りんご",
      "price" : 100,
      "tags" : [
        {
          "name" : "food"
        },
        {
          "name" : "fruit"
        }
      ]
    }
  ],
  "totalPrice" : 350
}

例えば、itemsの中にある1番目のitem.nameを取り出したいとします。

その場合、HCursorのdownField関数などを使用してフォーカスを移動させることによってデータを抽出します。

まず、downField("items")でitemsにフォーカスを移動させます。その後、downArray関数 or downN関数 を使用してitemsの中の1番目のitemにフォーカスを移動させます。

downArray関数は、以下のコメントにもあるように、配列の最初の要素にフォーカスを移動させます。

If the focus is an element in a JSON array, move to the first element.

downN関数は、配列のどの要素にフォーカスを移動させるか指定することができます。

配列の最初の要素にフォーカスを移動させたら、その要素がどういう型なのか as関数 で型を指定してデコードします。

その後、getOrElseでitemを取り出して、nameを取得してあげれば確認できます。

参考:

circe.github.io

最後に

play-jsonよりCirceの方が絶対に書きやすい!!