Accumulating nested decoding failures
See original GitHub issueHere’s an example where the obvious way to write a Decoder
causes
error accumulation to disappear:
ealed trait T
@JsonCodec
case class A(a1: Int, a2: Int) extends T
@JsonCodec
case class B(b1: Int, b2: Int) extends T
object Main extends App {
val normalDecoder: Decoder[T] =
Decoder.instance(cursor => {
cursor.downField("type").as[String].flatMap {
case "a" => cursor.as[A]
case "b" => cursor.as[B]
case other: String => Left(DecodingFailure(s"Unknown type: '$other'", Nil))
}
})
println(decodeAccumulating[T]("""{ "type": "a" }""")(normalDecoder))
}
=> Invalid(NonEmptyList(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(a1)))))
Decoder.instance
does not allow you to have several error messages
so we only get the first error.
We tried to fix this in our application and AFAICT the only way to do
so is to create a custom Decoder
, something that is only possible in
the io.circe
namespace as Decoder.decodeAccumulating
is
private[circe]
. This is what I ended up with:
package io.circe
import cats.data.{Validated, ValidatedNel}
import cats.data.Validated.Invalid
import io.circe.Decoder.Result
import io.circe.parser.decodeAccumulating
import io.circe.generic.JsonCodec
sealed trait T
@JsonCodec
case class A(a1: Int, a2: Int) extends T
@JsonCodec
case class B(b1: Int, b2: Int) extends T
object AccMain extends App {
def make[Ty](r: HCursor => AccumulatingDecoder.Result[Ty]): Decoder[Ty] =
new Decoder[Ty] {
override private[circe] def decodeAccumulating(c: HCursor): AccumulatingDecoder.Result[Ty] =
r(c)
override def apply(c: HCursor): Result[Ty] =
r(c).toEither.left.map(_.head) // Should be optimizable.
}
val accDecoder: Decoder[T] =
make[T] { (cursor: HCursor) =>
val y: ValidatedNel[DecodingFailure, String] = Validated.fromEither(cursor.downField("type").as[String]).toValidatedNel
y.andThen {
case "a" => Decoder[A].accumulating(cursor)
case "b" => Decoder[B].accumulating(cursor)
case other: String => Invalid(DecodingFailure(s"Unknown type: '$other'", Nil)).toValidatedNel
}
}
println(decodeAccumulating[T]("""{ "type": "a" }""")(accDecoder))
}
object OriginalMain extends App {
val normalDecoder: Decoder[T] =
Decoder.instance(cursor => {
cursor.downField("type").as[String].flatMap {
case "a" => cursor.as[A]
case "b" => cursor.as[B]
case other: String => Left(DecodingFailure(s"Unknown type: '$other'", Nil))
}
})
println(decodeAccumulating[T]("""{ "type": "a" }""")(normalDecoder))
}
AccMain => Invalid(NonEmptyList(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(a1))), DecodingFailure(Attempt to decode value on failed cursor, List(DownField(a2)))))
I have a couple of questions:
- Is there currently a better way to do this when writing a manual instance?
- Should a function like
make
be included in circe?
Issue Analytics
- State:
- Created 6 years ago
- Comments:10 (3 by maintainers)
Top GitHub Comments
So if I understand correctly, even using accumulating decoder, it’s not possible to have a single decoder return two failures directly?
+1