Search code examples
jsonscalaserializationplay-json

How can I parse an object of a bounded type T from JSON from an implicit request?


I have some simple messages with implicit Json.reads and Json.formats defined in their companion objects. All of these messages extend MyBaseMessage.

In other words, for any T <: MyBaseMessage, T is (de)serializable.

These messages represent simple CRUD operations to be performed on a cluster, so there's an Play server that will sit between a CLI sending JSON and the Cluster. Because the operations are simple, I should be able to make some very generic Actions on the Play side: when I receive JSON at an endpoint, deserialize the message according to the endpoint and forward that message to the cluster.

My ultimate goal is to do something like this:

// AddBooMessage extends MyBaseMessage
def addBoo = FooAction[AddBooMessage]  

// AddMooMessage extends MyBaseMessage
def addMoo = FooAction[AddMooMessage]

// etc. ...

So when a request is sent to the route corresponding to the addBoo message, the request's JSON will be parsed into an AddBooMessage message and pushed to the cluster. Repeat ad nauseam.

I have the following written:

  private def FooAction[T <: MyBaseMessage] = Action {
    implicit request =>
       parseAndForward[T](request)
  } 

  private def parseAndForward[T <: MyBaseMessage](request: Request[AnyContent]) = {
    val parsedRequest = Json.parse(request.body.toString).as[T]
    Logger.info(s"Got '$parsedRequest' request. Forwarding it to the Cluster.")
    sendToCluster(parsedRequest)
  }

But I find the following error:

No Json deserializer found for type T. Try to implement an implicit Reads or Format for this type.

However, all of these messages are serializable and have both Reads and Format defined for them.

I tried passing (implicit fjs: Reads[T]) to parseAndForward in hopes to implicitly provide the Reads required (though it should already be implicitly provided), but it didn't help.

How can I solve this problem?


Solution

  • JsValue#as[A] needs an implicit Reads[A] in order to deserialize JSON to some type A. That is, the error message you're getting is caused because the compiler can't guarantee there is a Reads[T] for any type T <: MyBaseMessage. Assuming sendToCluster is parameterized the same way, this can easily by fixed by simply requiring an implicit Reads[T] in each method call. It sounds like you were close, and just needed to take things a step further by requiring the Reads[T] from FooAction, as well (since that call is where the type is determined).

    private def FooAction[T <: MyBaseMessage : Reads] = Action { implicit request =>
      parseAndForward[T](request)
    } 
    
    private def parseAndForward[T <: MyBaseMessage : Reads](request: Request[AnyContent]) = {
      val parsedRequest = Json.parse(request.body.toString).as[T]
      Logger.info(s"Got '$parsedRequest' request. Forwarding it to the Cluster.")
      sendToCluster(parsedRequest) // Assuming this returns a `Future[Result]`
    }
    

    If your intention if you use the above code by manually supplying the type parameter, this will work just fine.


    There are some other improvements I think you can make here. First, if you always expect JSON, you should require the parse.json BodyParser. This will return a BadRequest if what is received isn't even JSON. Second, as will throw an exception if the received JSON cannot be deserialized into the expected type, you can use JsValue#validate to do this more safely and fold the result to handle the success and error cases explicitly. For example:

    private def FooAction[T <: MyBaseMessage] = Action.async(parse.json) { implicit request =>
      parseAndForward[T](request)
    } 
    
    private def parseAndForward[T <: MyBaseMessage](request: Request[JsValue]) = {
      request.body.validate[T].fold(
        error => {
          Logger.error(s"Error parsing request: $request")
          Future.successful(BadRequest)
        },
        parsed => {
          Logger.info(s"Got '$parsed' request. Forwarding it to the Cluster.")
          sendToCluster(parsed)
        }
      )
    }