1. 序章

Circe は、JSONの操作を簡素化するScalaライブラリであり、JSON文字列をScalaオブジェクトに簡単にデコードしたり、ScalaオブジェクトをJSONに変換したりできます。 ライブラリはオブジェクトのエンコーダーとデコーダーを自動的に生成するため、ScalaでJSONを操作するために必要なコード行が削減されます。

2. インストール

ライブラリをインストールするには、build.sbtファイルに数行追加します。

val circeVersion = "0.14.1"

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

3. JSONの検証

まず、 JSON文字列を検証して、有効なJSONオブジェクトが含まれているかどうかを確認します。 まず、有効なJSONオブジェクトを含む String を定義し、それをparse関数に渡します。

import io.circe._, io.circe.parser._

val jsonString =
"""
|{
| "textField": "textContent",
| "numericField": 123,
| "booleanField": true,
| "nestedObject": {
| "arrayField": [1, 2, 3]
| }
|}
|""".stripMargin

val parseResult: Either[ParsingFailure, Json] = parse(jsonString)

その結果、ParsingErrorまたはJsonオブジェクトのいずれかが取得されます。 次に、 matchステートメントを使用して、戻り値を区別します

parseResult match {
  case Left(parsingError) =>
    throw new IllegalArgumentException(s"Invalid JSON object: ${parsingError.message}")
  case Right(json) => // here we use the JSON object
}

4. 長い道のりでJSONプロパティにアクセスする

フィールド名を検索し、それらの値を期待されるデータ型に変換することで、解析されたJsonオブジェクトからフィールド値を抽出することができます。 ただし、このような方法は、冗長でエラーが発生しやすいコードを記述する必要があるため、お勧めしません。

前に定義したmatchステートメントを拡張して、neuroFieldの値を抽出してみましょう。

case Right(json) => 
  val numbers = json \\ "numericField"
  val firstNumber: Option[Option[JsonNumber]] =
    numbers.collectFirst{ case field => field.asNumber }
  val singleOption: Option[Int] = firstNumber.flatten.flatMap(_.toInt)

\\ 表記は、 findAllByKey 関数のエイリアスであり、Jsonオブジェクトのリストを返すことに注意してください。

その後、 collectFirst 関数を使用して、リストの最初の要素を含む可能性のあるOptionを取得します。 そのため、Optionが別のOptionにネストされてしまいます。 基になる数値を取得するには、 flatMap Option を実行し、JsonNumberOption[Int]に変換します。

明らかに、この過度に複雑な方法を使用する必要はありません。Circeはすべての複雑さを隠す単純化されたAPIを提供しているからです

5. ScalaオブジェクトをJSONに変換する

フィールドを手動で抽出して期待される形式に変換するのではなく、Circeコーデックを使用してJSONをScalaオブジェクトとの間で変換できます

ただし、コーデックでは、解析されたJSON文字列のフィールドに一致するケースクラスを作成する必要があります。

import io.circe._, io.circe.generic.semiauto._

case class Nested(arrayField: List[Int])

case class OurJson(
  textField: String,
  numericField: Int,
  booleanField: Boolean,
  nestedObject: Nested
)

ケースクラスが定義されると、クラスからデコーダーを派生させ、それを使用してJSON文字列を解析できます。 最初にNestedクラスのDecoderを定義することに注意してください。

implicit val nestedDecoder: Decoder[Nested] = deriveDecoder[Nested]
implicit val jsonDecoder: Decoder[OurJson] = deriveDecoder[OurJson]

val decoded = decode[OurJson](jsonString)

必要です ネスト JSON文字列では、 入れ子オブジェクト フィールドには別のJSONオブジェクトが含まれています。 したがって、すべてのJSONオブジェクトを個別のScalaオブジェクトに逆シリアル化し、個々のクラスを定義します。

6. JSONをScalaオブジェクトに変換する

同様に、クラスエンコーダーを派生させ、 ScalaオブジェクトをJsonオブジェクトに変換し、後でJSON文字列に変換することができます。 デコードされたケースクラスをJSON文字列に変換して戻しましょう。

implicit val nestedEncoder: Encoder[Nested] = deriveEncoder[Nested]
implicit val jsonEncoder: Encoder[OurJson] = deriveEncoder[OurJson]

decoded match {
  case Right(decodedJson) =>
    val jsonObject: Json = decodedJson.asJson
    val newJsonString = jsonObject.spaces2
}

7. オプションフィールドの操作

一部のJSONフィールドがオプションであるか、値が欠落している場合は、クラス定義を変更し、オプションクラスをフィールドタイプとして使用します。 neuroField booleanField 、およびarrayFieldが欠落しているJSON文字列を見てみましょう。

val jsonStringWithMissingFields =
"""{
| "textField" : "textContent",
| "nestedObject" : {
| "arrayField" : null
| }
|}""".stripMargin

クラス定義を変更せずにデコードしようとすると、「 Left(DecodingFailure(失敗したカーソルの値をデコードしようとしました。List(DownField(numericField))))」エラーが発生します。 したがって、クラス定義を変更する必要があります。

case class Nested(arrayField: Option[List[Int]])

case class OurJson(
  textField: String,
  numericField: Option[Int],
  booleanField: Option[Boolean],
  nestedObject: Nested
)

これで、デコーダーは不足している値を問題なく処理できます。

implicit val nestedDecoder: Decoder[Nested] = deriveDecoder[Nested]
implicit val jsonDecoder: Decoder[OurJson] = deriveDecoder[OurJson]

decode[OurJson](jsonStringWithMissingFields)

8. カスタムデコーダーの作成

ListOptionを使用する代わりに、欠落しているarrayFieldを空のリストに変換したい場合はどうすればよいでしょうか。 このような状況では、カスタムデコーダーを作成する必要があります。 カスタムデコーダーを作成するには、デコーダータイプを実装する必要があります。

この例では、arrayFieldnullであるかどうかを確認し、 NoneではなくNil(空のリスト)を返します。 フィールドが存在し、配列が含まれている場合は、変更を加えずに返します。

implicit val decodeNested: Decoder[Nested] = (c: HCursor) => for {
  arrayField <- c.downField("arrayField").as[Option[List[Int]]]
} yield {
  val flattenedArray = arrayField.getOrElse(Nil)
  Nested(flattenedArray)
}

9. カスタムデコーダーのテスト

Circeデコーダーとエンコーダーをカスタマイズするときは、コードをテストして、正しく機能することを確認する必要があります。 この例では、ScalaTestライブラリを使用してカスタムコードをテストします。

テストディレクトリで仕様クラスを定義し、入力JSON文字列を準備し、ケースクラスを定義して、デコーダー(カスタムデコーダーと自動生成されたデコーダーの両方)を実装します。

その後、考えられるすべてのケースについて4つのテストを記述します。値を持つ既存の配列、空の配列、nullフィールド、および欠落しているフィールドです。

"A custom decoder" should "decode a JSON with a null array value" in {
  decode[OurJson](jsonStringWithNullArray) shouldEqual Right(OurJson("textContent", None, None, Nested(Nil)))
}

it should "decode a JSON with a missing field" in {
  decode[OurJson](jsonStringWithMissingArray) shouldEqual Right(OurJson("textContent", None, None, Nested(Nil)))
}

it should "decode a JSON with an existing array value" in {
  decode[OurJson](jsonStringWithArray) shouldEqual Right(OurJson("textContent", None, None, Nested(List(1, 2))))
}

it should "decode a JSON with an empty array" in {
  decode[OurJson](jsonStringWithEmptyArray) shouldEqual Right(OurJson("textContent", None, None, Nested(Nil)))
}

10. 自動生成されたエンコーダ

デコーダーをカスタマイズする必要がない場合は、自動デコーダー派生を使用して、コードをさらに短縮することができます

io.circe.generic.auto ._ パッケージをインポートすると、既存のすべてのタイプのデコーダー(およびエンコーダー)が自動的に導出されるため、[を作成せずにJSONパーサーを使用できます。 X197X]デコーダーインスタンス:

import io.circe.generic.auto._, io.circe.parser

parser.decode[OurJson](jsonString)

11. 結論

Circeは、シンプルなAPIで実装の詳細を非表示にすることで、JSONの操作を簡素化するScalaライブラリです。 ただし、カスタムエンコーダーまたはカスタムデコーダーを作成するか、フィールド抽出コードを直接使用することで、いつでもその動作を変更できます。

いつものように、これらの例のコードはGitHubから入手できます。