简体   繁体   中英

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)

You may use Reads.list() .

val paymentSourceReader: Reads[PaymentSource] = __.read[JsObject].flatMap { o =>
  (__ \ "type").read[String].collect(ValidationError("UnknownPaymentSource")) {
    case "card" =>
      o.as[Card]
    case "bitcoin_receiver" =>
      o.as[BitcoinReceiver]
  }
}
  1. read[String] create error if no type pproperty.
  2. collect(ValidationError("UnknownPaymentSource") create error if type !(card|bitcoin_receiver).
  3. o.as[...] throw exception if can not cast

then use `paymentSourceReader'

val dataReader: Reads[List[PaymentSource]] = (__ \ "data").read[List[PaymentSource]](Reads.list(paymentSourceReader))

dataReader may be used in complex reader Reads[PaymentSource] with combinators for Sources or json.reads(dataReader) for JsResult[List[PaymentSource]]

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
  })
}

I had a similar problem. I wanted to join two JsResults into on JsSuccess. tupled and and helped me to fulfill this task. Both are part of the play.api.libs.functional package.

This is how you can join two JsResult into one:

import play.api.libs.json._
import play.api.libs.functional.syntax._

((__ \ 'id).validate[Long] and (__ \ 'name).validate[String]).tupled 
    match {
      case JsSuccess((id,name),_) => ...
      case err: JsError =>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM