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:
The equivalent Java code is as follows:
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:
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:
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:
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 Write
s definition, there is a macro automating the work for Scala case classes so that the preceding Reads
definition is completely equivalent to the following:
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:
The equivalent Java code is as follows:
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:
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:
The preceding code rejects JSON objects that have an empty name or negative price. You can try it in the REPL:
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:
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:
Alternatively, the absence of a value can also be represented by simply omitting the field itself:
If you choose to represent the absence of value using a field containing null
, the corresponding Reads
definition is the following:
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
:
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:
A naive Reads[Category]
definition can be the following:
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:
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
:
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
.