Search code examples
scalagenericsakka-httpplay-json

Generic REST client in Scala


I'm a bit new to Scala and I'm trying to write a generic client for a RESTful api I would like to use. I'm able to provide concrete Reads[T] and Writes[T] for the specific case classes I would like to instantiate my client for, however the compiler expects to find a Reads[T] and Writes[T] for any type, not just the types I'm using. Some code to illustrate (I've omitted irrelevant sections):

My generic client:

class RestModule[T](resource: String, config: Config) ... with JsonSupport{
...
def create(item: T): Future[T] = {
    val appId = config.apiId
    val path = f"/$apiVersion%s/applications/$appId%s/$resource"

    Future {
      val itemJson = Json.toJson(item)
      itemJson.toString.getBytes
    } flatMap {
      post(path, _)
    } flatMap { response =>
      val status = response.status
      val contentType = response.entity.contentType

      status match {
        case Created => contentType match {
          case ContentTypes.`application/json` => {
            Unmarshal(response.entity).to[T]
          }
          case _ => Future.failed(new IOException(f"Wrong Content Type: $contentType"))
        }
        case _ => Future.failed(new IOException(f"HTTP Error: $status"))
      }
    }
...
}

JsonSupprt Trait:

trait JsonSupport {
    implicit val accountFormat = Json.format[Account]
}

I'm only ever instantiating as RestModule[Account]("accounts",config) but I get the error

Error:(36, 32) No Json serializer found for type T. Try to implement an implicit Writes or Format for this type.
  val itemJson = Json.toJson(item)
                           ^

Why does the compiler think it needs a Writes for type T when T can only ever be of type Account? Is there any way to work around this?


Solution

  • The reason why the compiler doesn't like what you're doing is to do with how implicit parameters are resolved and more crucially when they are resolved.

    Consider the snippet,

    Object MyFunc {
       def test()(implicit s: String): String = s
    }
    

    The implicit parameter only gets resolved by the parameter when the function is called, and basically is expanded as,

    MyFunc.test()(resolvedImplicit)
    

    In your particular case you actually call the function requiring the implicit and hence it looks for an implicit of T at that point in time. Since it can't find one in scope it fails to compile.

    In order to solve this issue, simply add the implicit parameter to the create method to tell the compiler to resolve it when you call create rather than toJson within create.

    Furthermore we can use scala's implicit rules to get the behaviour that you want.

    Let's take your trait Reads,

    trait Reads[A] {
    }
    
    object MyFunc {
      def create[A](a: A)(implicit reads: Reads[A]): Unit = ???
    }
    

    as we said befeore you can call it if the implicit is in scope. However, in this particular case where you have predefined reads we can actually put it in the companion object,

    object Reads {
      implicit val readsInt: Reads[Int] = ???
      implicit val readsString: Reads[String] = ???
    }
    

    This way when create is called, the user doesn't need to import or define any implicit vals when A is Int or String because scala automatically looks in the companion object for any implicit definitions if it can't find one in the current scope.