Search code examples
scalaplayframeworkargonaut

creating Writeable[Argonaut.Json] for play framework http response


I am trying to change the implementation of this function from using plays json library like so

def apply[T](action: => ApiResponse[T])(implicit tjs: Writes[T], ec: ExecutionContext): Future[Result] = {
    action.fold(
      err =>
        Status(err.statusCode) {
          JsObject(Seq(
            "status" -> JsString("error"),
            "statusCode" -> JsNumber(err.statusCode),
            "errors" -> Json.toJson(err.errors)
          ))
        },
      t =>
        Ok {
          JsObject(Seq(
            "status" -> JsString("ok"),
            "response" -> Json.toJson(t)
          ))
        }
    )
  }

to use argonaut like so

def apply[T](action: => ApiResponse[T])(implicit encodeJson: EncodeJson[T], ec: ExecutionContext): Future[Result] = {
    action.fold(
      err =>
        Status(err.statusCode) {
          Json(
          "status" -> jString("error"),
          "statusCode" -> jNumber(err.statusCode),
          "errors" -> err.errors.asJson
          )
        },
      t =>
        Ok {
          Json(
          "status" -> jString("ok"),
          "response" -> t.asJson
          )
        }
    )
  }

but I get

Cannot write an instance of argonaut.Json to HTTP response. Try to define a Writeable[argonaut.Json]

for both the Status{} block and Ok{} block and I got a helpful answer to this problem here https://groups.google.com/forum/#!topic/play-framework/vBMf72a10Zc

so I tried to create the implicit conversion like so

implicit def writeableOfArgonautJson(implicit codec: Codec): Writeable[Json] = {
      Writeable(jsval => codec.encode(jsval.toString))
    }

which I think converts the json object to a string and provides it to codec.encode which should convert it to Array[Bytes] but I get

Cannot guess the content type to use for argonaut.Json. Try to define a ContentTypeOf[argonaut.Json]

jsval.nospaces.getBytes also return Array[Bytes] so I dont know if that can be used to help at all

so while I think that last error message means I just need to tell play that it should use content type application.json I also feel like this might be an unnecessary rabbit hole and there should be an easier way to do this.

edit: it wasnt such a rabbit hole as defining the contentType has things compiling at least but I still would like to know if this is correct


Solution

  • You seem to have answered your own question, but to confirm, the Writable[A] is:

    1. how to convert a type A to Array[Bytes] and
    2. what content type to use in the response, which also requires
    3. the current character encoding

    The character encoding is taken care of by the implicit Codec instance, so you then need an implicit ContentTypeOf[A] where A is argonaunt.Json:

    implicit def contentTypeOf_ArgonautJson(implicit codec: Codec): ContentTypeOf[argonaut.Json] = {
      ContentTypeOf[argonaut.Json](Some(ContentTypes.JSON))
    }
    

    and then the Writable[A], which has a type constraint on A that there be an in-scope ContentTypeOf[A] (which you've just defined):

    implicit def writeableOf_ArgonautJson(implicit codec: Codec): Writeable[argonaut.Json] = {
      Writeable(jsval => codec.encode(jsval.toString))
    }
    

    And as you point out, there endeth the rabbit hole. And indeed, it certainly seems a bit of a diversion but not much extra code when you consider you can now just do Ok(myArgonautObject) in as many actions as you want without further conversion and header-setting boilerplate.

    Perhaps you could put those implicits in an ExtraJsonHelpers trait and mix it in to your controllers, for more boilerplate reduction.