Book Image

Play Framework Essentials

By : Julien Richard-Foy
Book Image

Play Framework Essentials

By: Julien Richard-Foy

Overview of this book

Table of Contents (14 chapters)
Play Framework Essentials
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Reading JSON requests


Now your web service is able to send JSON data representing the application data. However, clients cannot yet create new data by sending JSON requests; the create action is still not implemented. This action should read the JSON data of requests to extract the information required to create a new item, effectively create the item, and return a response telling the client whether its operation succeeded.

The first step consists in defining which information is required to create a new item:

case class CreateItem(name: String, price: Double)

The equivalent Java code is as follows:

public class CreateItem {
  public String name;
  public Double price;
}

The CreateItem data type just glues together the information needed to create a new item: a name and price. The Java version uses public fields so that it can automatically be handled by the Jackson object mapper.

The CreateItem data type is easy to work with in your server-side code, but it means nothing for HTTP clients that only send JSON blobs. So you also have to define a JSON structure corresponding to the CreateItem data type. A simple solution consists of representing a CreateItem instance with a JSON object by mapping each member of the CreateItem type with a member of the JSON object. That is, a JSON object with a member "name" that contains a string value and a member "price" that contains a number value.

The next step consists of defining how to convert a JSON object consistent with this structure into a CreateItem value.

In Scala, similar to the Writes[A] typeclass that defines how to serialize an A value into a JSON object, there is a Reads[A] typeclass that defines how to get an A value from a JSON object. This typeclass has one abstract method:

def reads(json: JsValue): JsResult[A]

The JsResult[A] type represents either a successful conversion, JsSuccess(a), or a unsuccessful conversion, JsError(errors), which contains a list of errors such as missing fields in the JSON source object. So the Reads[A] typeclass tells how to try to convert a JSON value to an A value.

Play provides Reads[A] values for common types such as String, Int, or Double. You can then combine them to define Reads[A] values for more complex types. For instance, you can define a Reads[CreateItem] value that tells how to try to convert a JSON value to a CreateItem value, as follows:

import play.api.libs.json.{__, Reads}
import play.api.libs.functional.syntax._

implicit val readsCreateItem: Reads[CreateItem] = (
  ((__ \ "name").read[String]) and
  ((__ \ "price").read[Double])
)(CreateItem.apply _)

This code combines the Reads[String] and Reads[Double] values using the and combinator. The (__ \ "name") expression is a JSON path referring to a member "name" so that the (__ \ "name").read[String] expression reads the "name" member as String and the (__ \ "price").read[Double] expression reads the "price" member as Double. Finally, these values are passed to the apply method of the CreateItem data type to make a CreateItem instance. Before showing how to use this JSON reader to effectively transform the content of a JSON HTTP request, let's give more details on the process of transforming JSON blobs to values. As our readsCreateItem type is built by combining two subreaders using and, it tries to apply all of them. If all succeed, the obtained values are passed to the CreateItem.apply function to build a CreateItem instance and the reader returns a JsSuccess[CreateItem] value. If one of the subreaders fails, the reader returns a JsError value.

Tip

The and combinator is not a method of Reads[A]. It is available thanks to an implicit conversion imported by play.api.libs.functional.syntax._. This import brings several other combinators, such as or, which succeeds if one of the two subreaders succeeds. These combinators are not specific to the JSON API, and this is why they are defined in a separate package.

In our case, both sub-readers look up a member in a JSON object, according to a path defined by the \ operator. Note that we can define longer paths by chaining the \ operator. Consider, for instance, the following expression that defines a path locating a member "latitude" nested in a "position" member of a JSON object:

__ \ "position" \ "latitude"

Just like the Writes definition, the readsCreateItem definition is quite mechanical. We try to get each field of the CreateItem case class from a field of the same name in the JSON object. Just like the Writes definition, there is a macro automating the work for Scala case classes so that the preceding Reads definition is completely equivalent to the following:

implicit val readsCreateItem = Json.reads[CreateItem]

In Java, the Jackson mapper is used to convert JSON data to POJOs using reflection, so you don't need to provide similar definitions.

Finally, the last step consists of making the create action interpret request content as JSON data and making a CreateItem value from this data:

val create = Action(parse.json) { implicit request =>
  request.body.validate[CreateItem] match {
    case JsSuccess(createItem, _) =>
      shop.create(createItem.name, createItem.price) match {
        case Some(item) => Ok(Json.toJson(item))
        case None => InternalServerError
      }
    case JsError(errors) =>
      BadRequest
  }
}

The equivalent Java code is as follows:

import play.mvc.BodyParser;

@BodyParser.Of(BodyParser.Json.class)
public static Result create() {
  JsonNode json = request().body().asJson();
  CreateItem createItem;
  try {
    createItem = Json.fromJson(json, CreateItem.class);
  } catch(RuntimeException e) {
    return badRequest();
  }
  Item item = shop.create(createItem.name, createItem.price);
  if (item != null) {
    return ok(Json.toJson(item));
  } else {
    return internalServerError();
  }
}

There are three important points to note in the preceding code. First, we tell the create action to interpret the request body as JSON data by supplying the parse.json value to the Action builder (or in Java, by annotating the method with @BodyParser.Of(BodyParser.Json.class)). In Play, the component responsible for interpreting the body of an HTTP request is named body parser. By default, actions use a tolerant body parser that will be able to parse the request content as JSON, XML, or URL-encoded form or multipart form data, but you can force the use of a specific body parser by supplying it as the first parameter of your action builder (or by using the @BodyParser.Of annotation in Java). The advantage is that within the body of your request, you are guaranteed that the request body (available as the body field on the request value) has the right type. If the request body cannot be parsed by the body parser, Play returns an error response with the status 400 (Bad Request).

Secondly, in the Scala version, the second parameter passed to the Action builder is not a block of the Result type, as with previous actions, but a function of type Request[A] => Result. Actually, actions are essentially functions from HTTP requests (represented by the Request[A] type) to HTTP responses (Result). The previous way to use the Action builder (by just passing it a block of type Result) was just a convenient shorthand for writing an action ignoring its request parameter. The type parameter, A, in Request[A] represents the type of the request body. In our case, because we use the parse.json body parser, we actually have a request of type Request[JsValue]; the request.body expression has type JsValue. The default body parser produces requests of the type Request[AnyContent], whose body can contain JSON or XML content as described previously. In Java, Play sets up a context before calling your action code (just after the routing process) so that within a controller, you can always refer to the current HTTP request by using the request() method.

Thirdly, we make the CreateItem value from this request body by calling request.body.validate[CreateItem] (or Json.fromJson(json, CreateItem.class) in Java). The Scala version returns a JsResult value; this type can either be JsSuccess if the CreateItem object can be created from the JSON data (using the Reads[CreateItem] value available in the implicit scope), or JsError if the process failed. In Java, the result is simply null in the case of an error.

Note

In Scala, there is a body parser that not only parses the request body a JSON blob but also validates it according to a reader definition and returns a 400 (Bad Request) response in the case of a failure so that the previous Scala code is equivalent to the following shorter version:

val create = Action(parse.json[CreateItem]) { implicit request =>
  shop.create(request.body.name, request.body.price) match {
    case Some(item) => Ok(Json.toJson(item))
    case None => InternalServerError
  }
}

Validating JSON data

At this point, your clients can consult the items of the shop and create new items. What happens if one tries to create an item with an empty name or a negative price? Your application should not accept such requests. More precisely, it should reject them with the 400 (Bad Request) error.

To achieve this, you have to perform validation on data submitted by clients. You should implement this validation process in the business layer, but implementing it in the controller layer gives you the advantages of detecting errors earlier and error messages can directly refer to the structure of the submitted JSON data so that they can be more precise for clients.

In Java, the Jackson API provides nothing to check this kind of validation. The recommended way is to validate data after it has been transformed into a POJO. This process is described in Chapter 3, Turning a Web Service into a Web Application. In Scala, adding a validation step in our CreateItem reader requires a few modifications. Indeed, the Reads[A] data type already gives us the opportunity to report errors when the type of coercion process fails. We can also leverage this opportunity to report business validation errors. Incidentally, the Play JSON API provides combinators for common errors (such as minimum and maximum values and length verification) so that we can forbid negative prices and empty item names, as follows:

implicit val readsCreateItem = (
  (__ \ "name").read(Reads.minLength[String](1)) and
  (__ \ "price").read(Reads.min[Double](0))
)(CreateItem.apply _)

The preceding code rejects JSON objects that have an empty name or negative price. You can try it in the REPL:

scala> Json.obj("name" -> "", "price" -> -42).validate[CreateItem]
res1: play.api.libs.json.JsResult[controllers.CreateItem] = JsError(List(
    (/price,List(ValidationError(error.min,WrappedArray(0.0)))), 
    (/name,List(ValidationError(error.minLength,WrappedArray(1))))))

The returned object describes two errors: the first error is related to the price field; it has the "error.min" key and additional data, 0.0. The second error is related to the name field; it has the "error.minLength" key and an additional data, 1.

The Reads.minLength and Reads.min validators are predefined validators but you can define your own validators using the filter method of the Reads object.

Handling optional values and recursive types

Consider the following data type representing an item with an optional description:

case class Item(name: String, price: Double, description: Option[String])

In Scala, optional values are represented with the Option[A] type. In JSON, though you can perfectly represent them in a similar way, optional fields are often modeled using null to represent the absence of a value:

{ "name": "Foo", "price": 42, "description": null }

Alternatively, the absence of a value can also be represented by simply omitting the field itself:

{ "name": "Foo", "price": 42 }

If you choose to represent the absence of value using a field containing null, the corresponding Reads definition is the following:

(__ \ "name").read[String] and
(__ \ "price).read[Double] and
(__ \ "description").read(Reads.optionWithNull[String])

The optionWithNull reads combinator turns a Reads[A] into a Reads[Option[A]] by successfully mapping null to None. Note that if the description field is not present in the read JSON object, the validation fails. If you want to support field omission to represent the absence of value, then you have to use readNullable instead of read:

(__ \ "name").read[String] and
(__ \ "price).read[Double] and
(__ \ "description").readNullable[String]

This is because read requires the field to be present before invoking the corresponding validation. readNullable relaxes this constraint.

Now, consider the following recursive data type representing categories of items. Categories can have subcategories:

case class Category(name: String, subcategories: Seq[Category])

A naive Reads[Category] definition can be the following:

implicit val readsCategory: Reads[Category] = (
  (__ \ "name).read[String] and
  (__ \ "subcategories").read(Reads.seq[Category])
)(Category.apply _)

The seq combinator turns Reads[A] into Reads[Seq[A]]. The preceding code compiles fine; however, at run-time it will fail when reading a JSON object that contains subcategories:

scala> Json.obj(
  "name" -> "foo",
  "subcategories" -> Json.arr(
    Json.obj(
      "name" -> "bar",
      "subcategories" -> Json.arr()
    )
  )
).validate[Category]
java.lang.NullPointerException
        at play.api.libs.json.Json$.fromJson(Json.scala:115)
        …

What happened? Well, the seq[Category] combinatory uses the Reads[Category] instance before it has been fully defined, hence the null value and NullPointerException!

Turning implicit val readsCategory into implicit lazy val readsCategory to avoid the NullPointerException will not solve the heart of the problem; Reads[Category] will still be defined in terms of itself, leading to an infinite loop! Fortunately, this issue can be solved by using lazyRead instead of read:

implicit val readsCategory: Reads[Category] = (
  (__ \ "name).read[String] and
  (__ \ "subcategories").lazyRead(Reads.seq[Category])
)(Category.apply _)

The lazyRead combinator is exactly the same as read, but uses a byname parameter that is not evaluated until needed, thus preventing the infinite recursion in the case of recursive Reads.