LoginSignup
2
0

More than 3 years have passed since last update.

存在しているかも知れないプロパティとnullableな値をcirceで扱う

Posted at

HTTP PATCHの様に部分的にリソースを更新したい。つまり以下のようなvalueプロパティがあったりなかったりnullだったりするJSONを取り扱いたい。
StackOverflowにHow to write a custom decoder for [Option[Option[A]] in Circe?というピンポイントの質問が上がっていたが、良い感じに解決出来ていなかったため解決方法を記載する。

[
  {
    "id":1,
    "value":"something"
  },
  {
    "id":2,
    "value": null
  },
  {
    "id":3
  }
]

case class PatchModel(id:Long, value:Option[String])

まずタイトルの通りではないが、valueがnullとなるのを期待して見出しの通りに書いてみる。

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

case class PatchModel(id: Long, value: Option[String])

val json_case_one = """{ "id":1, "value":"something" }"""
val json_case_two = """{ "id":2, "value": null }"""
val json_case_three = """{ "id":3 }"""

println(parse(json_case_one).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(1,Some(something)))
println(parse(json_case_two).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(2,None))
println(parse(json_case_three).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(3,None))

上手く実行されているように見えるが、これは上手くいっていない。
json_case_twoとjson_case_threeでプロパティの有無という大きな違いがあるのに、valueが両方ともNONEになっている。PatchModel({100,None)と来たときにプロパティがあったのかvalueがnullであるか判断が出来ないのである。
PatchModel(3,null)が来ることを想定していたが、circeはnullを許さない

Option[Option[A]]

nullが返ってくるなんて浅はかな思考を遠くに放り投げ、プロパティがnullかもしれないし値もnullかもしれないということで以下のように定義をし直した。

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

case class PatchModel(id: Long, value: Option[Option[String]])

val json_case_one = """{ "id":1, "value":"something" }"""
val json_case_two = """{ "id":2, "value": null }"""
val json_case_three = """{ "id":3 }"""

println(parse(json_case_one).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(1,Some(Some(something))))
println(parse(json_case_two).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(2,None))
println(parse(json_case_three).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(3,None))

json_case_oneの結果はPatchModel(1,Some(Some(something)))となっており想定通り。
しかしjson_case_twoとjson_case_threeのvalueは同じになっている。
as[PatchModel]の引数に何か入れてどうにか出来ないかと考えたが、asの定義は以下の通りである。

def as[A](implicit d: Decoder[A]): Decoder.Result[A]

つまりDecoderを自分で定義するしかない。

Decoder[Option[Option[A]]]

以下のようにDecoder[Option[Option[A]]]を定義してみた。

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

implicit def nullablePropertyDefinitionDecoder[A](implicit decoder: Decoder[A]): Decoder[Option[Option[A]]] = new Decoder[Option[Option[A]]] {
      override def apply(c: HCursor): Result[Option[Option[A]]] = {
        c.success.map(f => f.as[Option[A]]).fold[Result[Option[Option[A]]]](Right(None))(x => x.map(Some(_)))
      }
    }

case class PatchModel(id: Long, value: Option[Option[String]])

val json_case_one = """{ "id":1, "value":"something" }"""
val json_case_two = """{ "id":2, "value": null }"""
val json_case_three = """{ "id":3 }"""

println(parse(json_case_one).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(1,Some(Some(something))))
println(parse(json_case_two).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(2,Some(None)))
println(parse(json_case_three).flatMap(json => json.as[PatchModel]))
// Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(value))))

json_case_one, json_case_twoは予想通りであるが、json_case_threeで失敗している。
なんでだろう?と考えていたときに、デフォルトで用意されているDecoder[Option[A]がプロパティが存在しない場合に動いていたことを思い出した。

完成

Decoder[Option[A]]のコードを参考にして書いたものが以下のものである。

import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._
import cats.data.Validated
import cats.data.Validated._

implicit def nullablePropertyDefinitionDecoder[A](implicit decoder: Decoder[Option[A]]): Decoder[Option[Option[A]]] = new Decoder[Option[Option[A]]] {
  override def apply(c: HCursor): Result[Option[Option[A]]] = tryDecode(c)

  override def tryDecode(c: ACursor): Decoder.Result[Option[Option[A]]] = c match {
    case c: HCursor =>
      decoder(c) match {
        case Right(a) => Right(Some(a))
        case Left(df) => Left(df)
      }
    case c: FailedCursor =>
      if (!c.incorrectFocus) Right(None) else Left(DecodingFailure("[A]Option[A]", c.history))
  }

    override def tryDecodeAccumulating(c: ACursor): AccumulatingResult[Option[Option[A]]] = c match {
    case c: HCursor =>
      decoder.decodeAccumulating(c) match {
        case Valid(a) => Valid(Some(a))
        case i@Invalid(_) => i
      }
    case c: FailedCursor =>
      if (!c.incorrectFocus) Valid(None)
      else Validated.invalidNel(DecodingFailure("[A]Option[Option[A]]", c.history))
      }
}

case class PatchModel(id: Long, value: Either[ {}, Option[String]])

val json_case_one = """{ "id":1, "value":"something" }"""
val json_case_two = """{ "id":2, "value": null }"""
val json_case_three = """{ "id":3 }"""

println(parse(json_case_one).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(1,Some(Some(something))))
println(parse(json_case_two).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(2,Some(None)))
println(parse(json_case_three).flatMap(json => json.as[PatchModel]))
// Right(PatchModel(3,None))

今度は思い通りの結果となっている。
最初にoverrideしたapply(c: HCursor) はPropertyが存在している時に呼ばれるメソッドであり、c.success と書いても既に成功している為意味が無かった。通常はプロパティが存在していることを前提に処理すれば問題ないためこのようになっているのだろう。
このため成功しているか失敗しているか分からないまま呼び出されるtryDecode(c: ACursor)をoverrideする必要があった。
tryDecode(c: Acursor)をoverrideする際には、tryDecodeAccumulatingも実装せよとドキュメントに書いてあったためそちらも実装をしている。
ACorsorとHCursorの違いについては、Traversing and modifying JSONに記載されている。

おわりに

存在しているかも知れないプロパティとnullableな値を良い感じに扱うDecoderを実装することが出来た。
しかしOptionがネストしているのは気持ち悪いので、このアプローチ自体が正しいものか疑問に思っている。
あと少し改良するならEither[何も無いことを表す型,Option[A]]が良かったのかも知れない。

2
0
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
2
0