2

I'm trying to implement basically the same thing that is discussed here, but in my specific situation, it's not working.

Currently I have a function that validates a JSON response from our server. The problem is, it has the JSON type hardcoded into the method:

    def fakeRequest[A: Writes](target: () => Call, requestObject: A): Any = {
        route(FakeRequest(target()).withJsonBody(Json.toJson(requestObject))) match {
            // ... stuff happens
            Json.parse(contentAsString(response)).validate[GPInviteResponse]
                                                           ^

Note the hardcoded GPInviteResponse type.

So, to make this a totally generic and reusable method, it would be great to pass in the type that is being validated.

I tried this:

    def fakeRequest[A: Writes, B](target: () => Call, requestObject: A, responseType: B): Any = {
        route(FakeRequest(target()).withJsonBody(Json.toJson(requestObject))) match {
            // ... stuff happens
            Json.parse(contentAsString(response)).validate[B]
                                                           ^

That almost works, but I get a No Json deserializer found for type B. Makes sense, so I changed it to:

    def fakeRequest[A: Writes, B: Reads](target: () => Call, requestObject: A, responseType: B): Any = {

But, now I get No Json deserializer found for type controllers.GPInviteResponse.type. So the question is: Is it possible to pass a type like this (or is there some other magic to make it work)?

There is a deserializer defined for the type... I've re-read it half a dozen times to make sure there's no typo:

case class GPInviteResponse(inviteSent: Boolean, URL: Option[String], error: Option[GPRequestError] = None) {
    def this(error: GPRequestError) = this(false, None, Option(error))
}

object GPInviteResponse {
    implicit val readsInviteResponse = Json.reads[GPInviteResponse]
    implicit val writesInviteResponse = Json.writes[GPInviteResponse]
}

Edit

Included below is a simplified test case that demonstrates the problem. At the moment, this won't compile (error is shown below). I think I understand why it won't work (vaguely) but I have no idea regarding a solution. My theory as to why it won't work: While the provided type GPInviteRequest does have an implicit Reads/Writes method, Scala cannot make the connection that an instance B is actually a GPInviteRequest so it concludes that B does not have Reads/Writes.

case class GPInviteResponse(inviteSent: Boolean)
object GPInviteResponse {
    implicit val readsInviteResponse = Json.reads[GPInviteResponse]
    implicit val writesInviteResponse = Json.writes[GPInviteResponse]
}
class TestInviteServices extends PlaySpecification {
    "try to validate a type" in {
        tryToValidate(GPInviteRequest(true))
    }
    def tryToValidate[B: Reads, Writes](i: B) = {
        val json = Json.toJson(i).toString
        Json.parse(json).validate[B].isSuccess must beTrue
    }
}

The above test yields:

[error] /Users/zbeckman/Projects/Glimpulse/Server-2/project/glimpulse-server/test/application/TestInviteServices.scala:46: No Json serializer found for type B. Try to implement an implicit Writes or Format for this type. [error] val json = Json.toJson(i).toString [error] ^ [error] /Users/zbeckman/Projects/Glimpulse/Server-2/project/glimpulse-server/test/application/TestInviteServices.scala:133: No Json deserializer found for type controllers.GPInviteResponse.type. Try to implement an implicit Reads or Format for this type. [error] fakeRequest(controllers.routes.GPInviteService.invite, i, GPInviteResponse) match { [error] ^

Zaphod
  • 1,387
  • 2
  • 17
  • 33

2 Answers2

2

Your error message:

No Json serializer found for type B. Try to implement an implicit Writes or Format for this type.

In this function, how is the toJson method supposed to know how to serialize your type B?

   def tryToValidate[B: Reads, Writes](i: B) = {
        val json = Json.toJson(i).toString
        Json.parse(json).validate[B].isSuccess must beTrue
    }

You haven't pulled in a writer/reader into scope, the compiler doesn't know where to look for one and that's why it's telling you to implement one. Here's a quick solution

case class GPInviteResponse(inviteSent: Boolean)
object GPInviteResponse {
  implicit val format = Json.format[GPInviteResponse]
}

def tryToValidate[B](i: B)(implicit format: Format[B]) = {
  val json = Json.toJson(i).toString
  Json.parse(json).validate[B]
}

Note: using the format method is equivalent to defining both a reads and writes.

Now, there is an implicit formatter for B in scope, so the compiler knows where to find it and injects it into the validate method which takes a reader implicitly:

// From play.api.libs.json

def validate[T](implicit rds: Reads[T]): JsResult[T] = rds.reads(this)

Edit

You can add type parameters to your function and then reference them in the validate[T] method, like so:

// Define another case class to use in the example
case class Foo(bar: String)
object Foo {
  implicit val format = Json.format[Foo]
}

def tryToValidate[B, C](implicit f1: Format[B], f2: Format[C]) = {

  val j1 = """{"inviteSent":true}"""
  val j2 = """{"bar":"foobar"}""" // 

  Json.parse(j1).validate[B]
  Json.parse(j2).validate[C]
}

// Example call
tryToValidate[GPInviteResponse, Foo]
goralph
  • 1,076
  • 9
  • 18
  • I didn't realize format() was basically reads/writes together, thanks. This is a great answer... but, I'm having trouble applying it to the "real world" case, which is slightly more complicated. This function signature: `def fakeRequest[A, B](target: () => Call, request: A, response: B)(implicit f1: Format[A], f2: Format[B])` is giving the same error as before ("No Json formatter found for GPInviteResponse.type"). It seems adding the second type (separating the request and response types) has complicated it. – Zaphod May 17 '15 at 19:51
  • Ah, I see the problem now: In the example, an instance is passed in. In my "real world" case, I'm specifying a **type** (not an instance). Is there a way to pass the actual **type** (eg. `GPInviteRequest`) and not an instance? – Zaphod May 17 '15 at 20:02
1

Try it this way :

def tryToValidate[B](i: B)(implicit format : Format[B]) = {
        val json = Json.toJson(i).toString
        Json.parse(json).validate[B].isSuccess must beTrue
    }
Bhavya Latha Bandaru
  • 1,078
  • 12
  • 21