JSON, as we discovered in previous chapters, is becoming the de-facto language for communicating structured data over HTTP. If you develop a web application or a web API, it is likely that you will have to consume or emit JSON, or both.
In Chapter 7, Web APIs, we learned how to parse JSON through json4s
. The Play framework includes its own JSON parser and emitter. Fortunately, it behaves in much the same way as json4s
.
Let's imagine that we are building an API that summarizes information about GitHub repositories. Our API will emit a JSON array listing a user's repositories when queried about a specific user (much like the GitHub API, but with just a subset of fields).
Let's start by defining a model for the repository. In Play applications, models are normally stored in the folder app/models
, in the models
package:
// app/models/Repo.scala package models case class Repo ( val name:String, val language:String, val isFork: Boolean, val size: Long )
Let's add a route to our application that serves arrays of repos for a particular user. In conf/routes
, add the following line:
// conf/routes GET /api/repos/:username controllers.Api.repos(username)
Let's now implement the framework for the controller. We will create a new controller for our API, imaginatively called Api
. For now, we will just have the controller return dummy data. This is what the code looks like (we will explain the details shortly):
// app/controllers/Api.scala package controllers import play.api._ import play.api.mvc._ import play.api.libs.json._ import models.Repo class Api extends Controller { // Some dummy data. val data = List[Repo]( Repo("dotty", "Scala", true, 14315), Repo("frontend", "JavaScript", true, 392) ) // Typeclass for converting Repo -> JSON implicit val writesRepos = new Writes[Repo] { def writes(repo:Repo) = Json.obj( "name" -> repo.name, "language" -> repo.language, "is_fork" -> repo.isFork, "size" -> repo.size ) } // The controller def repos(username:String) = Action { val repoArray = Json.toJson(data) // toJson(data) relies on existence of // `Writes[List[Repo]]` type class in scope Ok(repoArray) } }
If you point your web browser to 127.0.0.1:9000/api/repos/odersky
, you should now see the following JSON object:
[{"name":"dotty","language":"Scala","is_fork":true,"size":14315},{"name":"frontend","language":"JavaScript","is_fork":true,"size":392}]
The only tricky part of this code is the conversion from Repo
to JSON. We call Json.toJson
on data
, an instance of type List[Repo]
. The toJson
method relies on the existence of a type class Writes[T]
for the type T
passed to it.
The Play framework makes extensive use of type classes to define how to convert models to specific formats. Recall that we learnt how to write type classes in the context of SQL and MongoDB. The Play framework's expectations are very similar: for the Json.toJson
method to work on an instance of type Repo
, there must be a Writes[Repo]
implementation available that specifies how to transform Repo
objects to JSON.
In the Play framework, the Writes[T]
type class defines a single method:
trait Writes[T] { def writes(obj:T):Json }
Writes
methods for built-in simple types and for collections are already built into the Play framework, so we do not need to worry about defining Writes[Boolean]
, for instance.
The Writes[Repo]
instance is commonly defined either directly in the controller, if it is just used for that controller, or in the Repo
companion object, where it can be used across several controllers. For simplicity, we just embedded it in the controller.
Note how type-classes allow for separation of concerns. The model just defines the Repo
type, without attaching any behavior. The Writes[Repo]
type class just knows how to convert from a Repo
instance to JSON, but knows nothing of the context in which it is used. Finally, the controller just knows how to create a JSON HTTP response.
Congratulations, you have just defined a web API that returns JSON! In the next section, we will learn how to fetch data from the GitHub web API to avoid constantly returning the same array.