'select and use appropriate decoder as decoder for new item while preserving error accumulation
If I have some point where my logic needs to diverge and thus have need to select 1 of some set of possible decoders to use, this issue can arise.
Consider this code snippet:
import cats.implicits.{catsSyntaxTuple3Semigroupal, catsSyntaxTuple2Semigroupal}
import io.circe.{Decoder, HCursor}
sealed trait Example
object Example {
case class OptionOne(
name: String,
time: String,
platform: String
) extends Example
object OptionOne {
implicit val decoder: Decoder[OptionOne] = {
(
Decoder[String].prepare(_.downField("event")),
Decoder[String].prepare(_.downField("time")),
Decoder[String].prepare(_.downField("platform"))
)
.mapN(OptionOne(_, _, _))
}
}
case class OptionTwo(
name: String,
time: String
) extends Example
object OptionTwo {
implicit val decoder: Decoder[OptionTwo] = {
(
Decoder[String].prepare(_.downField("event")),
Decoder[String].prepare(_.downField("time"))
)
.mapN(OptionTwo(_, _))
}
}
implicit val decoder: Decoder[Example] = (a: HCursor) => {
a.get[String]("event").flatMap {
case "some_precise_name" => OptionTwo.decoder(a)
case _ => OptionOne.decoder(a)
}
}
}
If conditionally based on the value of the event value I want to have either OptionOne or OptionTwo selected, the decoder for Example does successfully do this. Additionally, it does return the decoding failure if there is a problem. However, if you want the errors to accumulate, this is where the problem emerges.
If you consider this JSON object for testing:
Json.obj(
("event", "randomEvent".asJson),
("time", 8.asJson),
("platform", 5.asJson)
)
and pass this in as such: Example.decoder.decodeAccumulating(inJsonInvalid.hcursor), only the first error is returned rather than both errors expected.
So the question is this: how can this be done in a way that preserves the accumulation of errors?
[EDIT] to help with clarification, here is a test suite I would expect to pass for the solution to work:
class ExampleSuite
extends AnyFunSuite
with Matchers
with Inside {
test("Decoder -- OptionOne") {
val inJsonValidApp: Json =
Json.obj(
("event", "something".asJson),
("time", "2021-05-05 20:09:57.448".asJson),
("platform", "app".asJson),
("context", Json.obj(
("app_version", "5.7.9".asJson)
))
)
noException should be thrownBy OptionOne.decoder.decodeJson(inJsonValidApp)
inside(OptionOne.decoder.decodeJson(inJsonValidApp)) {
case Right(OptionOne(name, time, platform)) =>
name should be("something")
time should be("2021-05-05 20:09:57.448")
platform should be("app")
}
val inJsonInvalid =
Json.obj(
("event", "randomEvent".asJson),
("time", 8.asJson),
("platform", 5.asJson)
)
noException should be thrownBy OptionOne.decoder.decodeJson(inJsonInvalid)
inside(OptionOne.decoder.decodeJson(inJsonInvalid)) {
case Left(DecodingFailure(msg, ops)) =>
msg should be("String")
ops should be(List(DownField("time")))
}
noException should be thrownBy OptionOne.decoder.decodeAccumulating(inJsonInvalid.hcursor)
inside(OptionOne.decoder.decodeAccumulating(inJsonInvalid.hcursor)) {
case Invalid(NonEmptyList(head, tail)) =>
inside(head) {
case DecodingFailure(msg, ops) =>
msg should be("String")
ops should be(List(DownField("time")))
}
inside(tail) {
case List(DecodingFailure(msg, ops)) =>
msg should be("String")
ops should be(List(DownField("platform")))
}
}
}
test("Decoder -- OptionTwo") {
val inJsonValidApp: Json =
Json.obj(
("event", "something".asJson),
("time", "2021-05-05 20:09:57.448".asJson),
("platform", "app".asJson)
)
noException should be thrownBy OptionTwo.decoder.decodeJson(inJsonValidApp)
inside(OptionTwo.decoder.decodeJson(inJsonValidApp)) {
case Right(OptionTwo(name, time)) =>
name should be("something")
time should be("2021-05-05 20:09:57.448")
}
val inJsonInvalid =
Json.obj(
("event", "randomEvent".asJson),
("platform", "bee".asJson)
)
noException should be thrownBy OptionTwo.decoder.decodeJson(inJsonInvalid)
inside(OptionTwo.decoder.decodeJson(inJsonInvalid)) {
case Left(DecodingFailure(msg, ops)) =>
msg should be("Attempt to decode value on failed cursor")
ops should be(List(DownField("time")))
}
}
test("Decoder -- Example (valid)") {
val inJsonValidApp: Json =
Json.obj(
("event", "something".asJson),
("time", "2021-05-05 20:09:57.448".asJson),
("platform", "app".asJson)
)
noException should be thrownBy Example.decoder.decodeJson(inJsonValidApp)
inside(Example.decoder.decodeJson(inJsonValidApp)) {
case Right(OptionOne(name, time, platform)) =>
name should be("something")
time should be("2021-05-05 20:09:57.448")
platform should be("app")
}
val inJsonBE: Json =
Json.obj(
("event", "some_precise_name".asJson),
("time", "2021-05-05 20:09:57.448".asJson),
("platform", "app".asJson)
)
noException should be thrownBy Example.decoder.decodeJson(inJsonBE)
inside(Example.decoder.decodeJson(inJsonBE)) {
case Right(OptionTwo(name, time)) =>
name should be("some_precise_name")
time should be("2021-05-05 20:09:57.448")
}
}
test("Decoder -- Example (invalid, single)") {
val inJsonInvalid =
Json.obj(
("event", "randomEvent".asJson),
("time", 5.asJson),
("platform", "bee".asJson)
)
noException should be thrownBy Example.decoder.decodeJson(inJsonInvalid)
inside(Example.decoder.decodeJson(inJsonInvalid)) {
case Left(DecodingFailure(msg, ops)) =>
msg should be("String")
ops should be(List(DownField("time")))
}
}
test("Decoder -- Example (invalid, multiple)") {
val inJsonInvalid =
Json.obj(
("event", "randomEvent".asJson),
("time", 8.asJson),
("platform", 5.asJson)
)
noException should be thrownBy Example.decoder.decodeAccumulating(inJsonInvalid.hcursor)
inside(Example.decoder.decodeAccumulating(inJsonInvalid.hcursor)) {
case Invalid(NonEmptyList(head, tail)) =>
inside(head) {
case DecodingFailure(msg, ops) =>
msg should be("String")
ops should be(List(DownField("time")))
}
inside(tail) {
case List(DecodingFailure(msg, ops)) =>
msg should be("String")
ops should be(List(DownField("platform")))
}
}
}
}
Solution 1:[1]
since they are Decoders, you can combine them in a such way:
implicit val decoder: Decoder[Example] = List[Decoder[Example]](
Decoder[OptionOne].widen,
Decoder[OptionTwo].widen,
...
).reduceLeft(_ or _)
it will just try each one
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Alexey Rykhalskiy |
