How do I write a generic JSON parser in Play 2.7 for Scala that validates inbound requests?

264 views Asked by At

I have a Play 2.7 controller in Scala that validates inbound JSON requests against a case class schema, and reports inbound request payload errors (note that I extracted this sample from a larger codebase, attempting to preserve its correct compilability and functionality, though there may be minor mistakes):

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

import com.google.inject.Inject
import play.api.libs.json.{JsError, JsPath, JsSuccess, JsValue, Json, JsonValidationError}
import play.api.mvc.{AbstractController, Action, ControllerComponents, Request, Result}

class Controller @Inject() (playEC: ExecutionContext, cc: ControllerComponents) extends AbstractController(cc) {

  case class RequestBody(id: String)
  implicit val requestBodyFormat = Json.format[RequestBody]

  private val tryJsonParser = parse.tolerantText.map(text => Try(Json.parse(text)))(playEC)

  private def stringify(path: JsPath, errors: Seq[JsonValidationError]): String = {
    s"$path: [${errors.map(x => x.messages.mkString(",") + (if (x.args.size > 0) (": " + x.args.mkString(",")) else "")).mkString(";")}]"
  }

  private def runWithRequest(rawRequest: Request[Try[JsValue]], method: (RequestBody) => Future[Result]): Future[Result] = {
    rawRequest.body match {
      case Success(validBody) =>
        Json.fromJson[RequestBody](validBody) match {
          case JsSuccess(r, _) => method(r)
          case JsError(e) => Future.successful(BadRequest(Json.toJson(e.map(x => stringify(x._1, x._2)).head)))
        }
      case Failure(e) => Future.successful(BadRequest(Json.toJson(e.getMessage.replaceAll("\n", ""))))
    }
  }

  // POST request processor
  def handleRequest: Action[Try[JsValue]] = Action(tryJsonParser).async { request: Request[Try[JsValue]] =>
    runWithRequest(request, r => {
      Future.successful(Ok(r.id))
    })
  }
}

The validation works like this when sending a POST request to the "handleRequest" endpoint:

  • with the payload {malformed,,, I get a 400 response back with Unexpected character ('m' (code 109)): was expecting double-quote to start field name at [Source: (String)"{malformed,,"; line: 1, column: 3].
  • with the payload {} I get a 400 response back with /id: [error.path.missing]

What I would like to do is to make the parsing and validating generic, moving that logic into a utility class for the cleanest re-use possible in the handleRequest method. For example, something like this:

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

import com.google.inject.{Inject, Singleton}
import play.api.{Configuration, Logging}
import play.api.libs.json.{JsError, JsPath, JsSuccess, JsValue, Json, JsonValidationError}
import play.api.mvc.{AbstractController, Action, ActionBuilderImpl, AnyContent, BodyParsers, ControllerComponents, Request, Result}

object ParseAction {
  // TODO: how do I work this in?
  val tryJsonParser = parse.tolerantText.map(text => Try(Json.parse(text)))(playEC)
}

class ParseAction @Inject()(parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) {
  private def stringify(path: JsPath, errors: Seq[JsonValidationError]): String = {
    s"$path: [${errors.map(x => x.messages.mkString(",") + (if (x.args.size > 0) (": " + x.args.mkString(",")) else "")).mkString(";")}]"
  }

  // TODO: how do I make this method generic?
  override def invokeBlock[A](rawRequest: Request[A], block: (A) => Future[Result]) = {
    rawRequest.body match {
      case Success(validBody) =>
        Json.fromJson[A](validBody) match {
          case JsSuccess(r, _) => block(r).getFuture
          case JsError(e) => Future.successful(BadRequest(Json.toJson(e.map(x => stringify(x._1, x._2)).head)))
        }
      case Failure(e) => Future.successful(BadRequest(Json.toJson(e.getMessage.replaceAll("\n", ""))))
    }
  }
}

class Controller @Inject() (cc: ControllerComponents) extends AbstractController(cc) {

  case class RequestBody(id: String)
  implicit val requestBodyFormat = Json.format[RequestBody]

  // route processor
  def handleRequest = ParseAction.async { request: RequestBody =>
    Future.successful(Ok(r.id))
  }
}

I'm aware that this code doesn't compile as-is due to blatant Scala and Play API misuse rather than just small coding mistakes. I tried pulling from Play's own documentation about Action composition, but I have not had success in getting things right, so I left all of the pieces around hoping someone can help me to work them together into something that works.

How can I change this second code sample around to compile and behave functionally identically to the first code sample?

1

There are 1 answers

0
Valerii Rusakov On

I archived similar goal by using implicit class for ActionBuilder:

trait ActionBuilderImplicits {

  implicit class ExActionBuilder[P](actionBuilder: ActionBuilder[Request, P])(implicit cc: ControllerComponents) {

    def validateJson[A](implicit executionContext: ExecutionContext, reads: Reads[A]): ActionBuilder[Request, A] = {
      actionBuilder(cc.parsers.tolerantJson.validate(jsValue => {
        jsValue.validate.asEither.left
          .map(errors => BadRequest(JsError.toJson(errors)))
      }))
    }
  }

}

object ActionBuilderImplicits extends ActionBuilderImplicits

Then in controller you can import ActionBuilderImplicits and use it as

Action.validateJson[A].async { request =>
   processingService.process(request.body)
}

Here is request.body already type of A