Search code examples
jsonscalaplayframeworkplayframework-2.0play-json

Sequence a List[JsResult[A]] to a JsResult[List[A]]


I am attempting to make an API for stripe which involves a lot of mapping from Json to case classes (and vice versa). I have come across an issue where I end up with a List[JsResult[A]] (this is the result of mapping through a list of JObject's and doing some operations on them to map them to the appropriate case class). The code in question is below

case class Sources(data: List[PaymentSource],
                     hasMore: Boolean,
                     totalCount: Double,
                     url: String)

  implicit val sourcesReader: Reads[Sources] = {

    val dataAsList = (__ \ "data").read[List[JsObject]].flatMap{jsObjects =>
      val `jsResults` = jsObjects.map{jsObject =>
        val `type` = jsObject \ "type"

        val paymentSource: JsResult[PaymentSource] = `type` match {
          case JsString("card") =>
            Json.fromJson[Card](jsObject)
          case JsString("bitcoin_receiver") =>
            Json.fromJson[BitcoinReceiver](jsObject)
          case JsString(s) =>
            throw UnknownPaymentSource(s)
          case _ =>
            throw new IllegalArgumentException("Expected a Json Object")
        }

        paymentSource
      }

      jsResults

    }

The jsResults has a type of List[JsResult[A]], however to compose it properly with the reads we need to return either a JsResult[A] or a JsError.

Although its possible to do Json.fromJson[Card](jsObject).get instead of Json.fromJson[Card](jsObject), doing so means we lose the accumulative error handling in Play Json (it also means we are pushing the errors into runtime)


Solution

  • So, you can't turn a List[JsResult[A]] into JsResult[A], because what if you have multiple success results? That would mean you have multiple values for A. You can turn it into JsResult[List[A]], there are a few ways to do this, I'd probably do this:

    val allErrors = jsResults.collect {
      case JsError(errors) => errors
    }.flatten
    
    val jsResult = if (allErrors.nonEmpty) {
      JsError(allErrors)
    } else {
      JsSuccess(jsResults.collect {
        case JsSuccess(a, _) => a
      })
    }