4
votes

I am using Akka Http to make requests to a 3rd party API. The responses are "application/json", and I would like to use Akka Http to convert them to a custom case class. I would like to do something like this:

val request = RequestBuilding.Get("https://service.com/v1/api/items")

val response : Future[ItemsResponse] = http.singleRequest(request).flatMap({  response =>
  Unmarshal(response.entity).to[ItemsResponse]
})

This fails to compile, because I am missing an implicit unmarshaller of type akka.http.scaladsl.unmarshalling.Unmarshaller[akka.http.scaladsl.model.ResponseEntity, com.mycompany.models.ItemsResponse].

It's unclear to me what the idiomatic way to do this with akka http is. I am aware that I could use spray-json, but I'd like to understand how to do this without importing another library. It seems possible with Akka Http, but the documentation isn't clear (to me at least).

2

2 Answers

4
votes

The simplest way is to use spray-json as it comes as part of Akka HTTP:

import spray.json._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport

// change 2 to the number of attributes of ItemsResponse
implicit val ItemsResponseFormat = jsonFormat2(ItemsResponse)

This should make your existing code compile.

0
votes

I think your question is valid, and there are cases where avoiding extra dependencies makes sense. Mine is from making an authentication library, where I don't want to impose my JSON library preferences to the users of such library. The library needs JSON unmarshalling for understanding a token info response.

To the code! :)

case class TokenInfo private (uid: String, realm: String, scope: Seq[String])

object TokenInfo {
  private
  def parseOpt(s: String): Option[TokenInfo] = {

    util.parsing.json.JSON.parseFull(s) match {
      case Some(map: Map[String,Any] @unchecked) =>

        val tmp: Map[String,Any] = map.collect {
          case (k@ "uid",x: String) => k -> x
          case (k@ "realm",x: String) => k -> x
          case (k@ "scope",x: Seq[String] @unchecked) => k -> x
          // other keys are ignored
        }.toMap

        if (tmp.size == 3) {
          Some( TokenInfo( tmp("uid").asInstanceOf[String], tmp("realm").asInstanceOf[String], tmp("scope").asInstanceOf[Seq[String]]) )
        } else {
          None
        }

      case _ => None
    }
  }

  implicit
  val unm: FromEntityUnmarshaller[TokenInfo] = {
    PredefinedFromEntityUnmarshallers.stringUnmarshaller.map{ s => parseOpt(s).getOrElse{
      throw new RuntimeException(s"Unknown TokenInfo: $s")
    }}
  }
}

I chose to use util.parsing.json which comes within Scala. The other option was simply regex's, but in that case I'm either fixing the expected order of fields, or the code might get complex.