Search code examples
scalascalazargonaut

argonaut - separating failures and successes when extracting List[A]


I have an instance of DecodeJson[A] for a particular type, A, and I have an instance of Json, call it j. I'd like to do something like j.as[List[A]]. However, this JSON is coming to me from a 3rd party, and if some of the items in the array are missing fields, I'd like to just log errors for those items and collect only the items that can be properly parsed (instead of failing the entire decoding).

It seems to me like what I want to end up with is something like (List[(String, CursorHistory)], List[A]). That is, any successfully decoded elements will end up in the list on the right, and errors for any items that could not be successfully parsed will be accumulated on the left. This looks a lot like the MonadPlus.separate method in scalaz.

I can think of a couple ways of getting there. One way is to do something like:

j.as[List[Json]].map(_.map(_.as[A].result).separate)

This should give me a DecodeResult[List[(String, CursorHistory)], List[A]] as desired. However, that will throw away cursor information about where in the JSON array the items that failed were located. I could use something like zipWithIndex, but that starts to get pretty messy.

To me what seems like a better solution is to do something like this:

implicit def DecodeResultDecodeJson[A: DecodeJson]: DecodeJson[DecodeResult[A]] =
  decodeArr(_.as[A]) // outer DecodeResult is always a success

j.as[List[DecodeResult[A]]].map(_.map(_.result)).separate)

This will give me the same output type, but the CursorHistory should be populated to include the information about stepping into the elements of the array.

Here is an example:

val j = Json.array(List("1", "2", "foo").map(Json.jString): _*)
val r = j.as[List[DecodeResult[Int]]].map(_.map(_.result).separate)
// r is DecodeResult[(List[(String, CursorHistory)], List[Int])] =
// DecodeResult(\/-((List((Int,CursorHistory([->,->,\\]))),List(1, 2))))

This seems to behave reasonably (ignoring some terrible List concatenation that is probably happening). However, it seems like this sort of thing would be a fairly common use-case, so I'm wondering if there is a simpler or built-in way to do something like this. If not, I might submit a PR to argonaut for the DecodeJson[DecodeJson[A]] instance and see if anyone is interested.


Solution

  • You can use the HCursor decoder, rather than the Json decoder, which preserves the history:

    j.as[List[HCursor]].map(_.map(_.as[Int].result).separate)