circeのdecodeでデフォルト値を設定する

ScalaJSONライブラリ circe で、JSON を decode する際にフィールドが欠落していたらデフォルト値に置き換える方法について書きます。結構前からある機能なのでご存知の方も多いかもしれません。

つまり、こういうケースですね。

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

val json1 =
  """
    |{
    |  "name": "Taro",
    |  "body": "よろしくお願いします"
    |}
  """.stripMargin

val json2 =
  """
    |{
    |  "name": "Taro"
    |}
  """.stripMargin

final case class Message(
  name: String,
  body: String
)

// これはうまくいくけど
println(decode[Message](json1))
// => Right(Message(Taro,よろしくお願いします))

// フィールドが欠落しているので decode に失敗する。デフォルト値に置き換えたい。
println(decode[Message](json2))
// => Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(body))))

実行環境

今回実行した環境は以下になります。

  • Scala 2.12.6
  • circe 0.9.3

実現方法

方法1

最初は、case class のそれぞれのフィールドにデフォルト値を設定しておき、io.circe.generic.extras.Configuration の指定で欠落するフィールドをデフォルト値へ置き換える方法です。

build.sbt は以下のように、基本セットの他に circe-generic-extras を依存に追加します。

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

val circeVersion = "0.9.3"

そして、ポイントとなるのは、

  • io.circe.generic.auto._ ではなく、io.circe.generic.extras.auto._ を import する
  • implicit val customConfig: Configuration = Configuration.default.withDefaults を定義する

コードを見てみましょう。

import io.circe.generic.extras.auto._
import io.circe.generic.extras.Configuration
import io.circe.parser._

val json1 =
  """
    |{
    |  "name": "Taro"
    |}
  """.stripMargin

val json2 =
  """
    |{
    |  "body": "よろしくお願いします"
    |}
  """.stripMargin

val json3 = "{}"

// case class のフィールドにはデフォルト値が設定されている
final case class Message(
  name: String = "匿名希望",
  body: String = "ノーコメントです"
)

implicit val customConfig: Configuration = Configuration.default.withDefaults

println(decode[Message](json1)) // => Right(Message(Taro,ノーコメントです))
println(decode[Message](json2)) // => Right(Message(匿名希望,よろしくお願いします))
println(decode[Message](json3)) // => Right(Message(匿名希望,ノーコメントです))

このように欠落するフィールドに対してデフォルト値が置き換わりました。

方法2

もう一つの方法は、デフォルト値を設定しておいた case class を別途用意しておき、それで置き換えるやり方です。

build.sbt は 方法1 とは異なり、circe-generic-extras を依存に追加する必要はありません。

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

val circeVersion = "0.9.3"

そして、こうに書きます。

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

val json1 =
  """
    |{
    |  "name": "Taro"
    |}
  """.stripMargin

val json2 =
  """
    |{
    |  "body": "よろしくお願いします"
    |}
  """.stripMargin

val json3 = "{}"

// 方法1と違ってここではデフォルト値は設定していない
final case class Message(
  name: String,
  body: String
)

// 適用させるデフォルト値
lazy val defaultValue = Message("匿名希望", "ノーコメントです")

println(decode[Message => Message](json1).map(_(defaultValue)))
// => Right(Message(Taro,ノーコメントです))

println(decode[Message => Message](json2).map(_(defaultValue)))
// => Right(Message(匿名希望,よろしくお願いします))

println(decode[Message => Message](json3).map(_(defaultValue)))
// => Right(Message(匿名希望,ノーコメントです))

この方法でも欠落するフィールドに対してデフォルト値を置き換えることができました。ちなみに以下の issue にもあるように、方法1 の方が公式のやり方っぽいですね。

参考