Documentation

You are viewing the documentation for the 2.9.4 release in the 2.9.x series of releases. The latest stable release series is 3.0.x.

§JSON with HTTP

Play supports HTTP requests and responses with a content type of JSON by using the HTTP API in combination with the JSON library.

See HTTP Programming for details on Controllers, Actions, and routing.

We’ll demonstrate the necessary concepts by designing a simple RESTful web service to GET a list of entities and accept POSTs to create new entities. The service will use a content type of JSON for all data.

Here’s the model we’ll use for our service:

case class Location(lat: Double, long: Double)
object Location {
  def unapply(l: Location): Option[(Double, Double)] = Some(l.lat, l.long)
}

case class Place(name: String, location: Location)

object Place {
  var list: List[Place] = {
    List(
      Place(
        "Sandleford",
        Location(51.377797, -1.318965)
      ),
      Place(
        "Watership Down",
        Location(51.235685, -1.309197)
      )
    )
  }

  def save(place: Place): Unit = {
    list = list ::: List(place)
  }

  def unapply(p: Place): Option[(String, Location)] = Some(p.name, p.location)
}

§Serving a list of entities in JSON

We’ll start by adding the necessary imports to our controller.

import play.api.mvc._

class HomeController @Inject() (cc: ControllerComponents) extends AbstractController(cc) {}

Before we write our Action, we’ll need the plumbing for doing conversion from our model to a JsValue representation. This is accomplished by defining an implicit Writes[Place].

implicit val locationWrites: Writes[Location] =
  (JsPath \ "lat").write[Double].and((JsPath \ "long").write[Double])(unlift(Location.unapply))

implicit val placeWrites: Writes[Place] =
  (JsPath \ "name").write[String].and((JsPath \ "location").write[Location])(unlift(Place.unapply))

Next we write our Action:

def listPlaces() = Action {
  val json = Json.toJson(Place.list)
  Ok(json)
}

The Action retrieves a list of Place objects, converts them to a JsValue using Json.toJson with our implicit Writes[Place], and returns this as the body of the result. Play will recognize the result as JSON and set the appropriate Content-Type header and body value for the response.

The last step is to add a route for our Action in conf/routes:

GET   /places               controllers.Application.listPlaces

We can test the action by making a request with a browser or HTTP tool. This example uses the unix command line tool cURL.

curl --include http://localhost:9000/places

Response:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 141

[{"name":"Sandleford","location":{"lat":51.377797,"long":-1.318965}},{"name":"Watership Down","location":{"lat":51.235685,"long":-1.309197}}]

§Creating a new entity instance in JSON

For this Action we’ll need to define an implicit Reads[Place] to convert a JsValue to our model.

implicit val locationReads: Reads[Location] =
  (JsPath \ "lat").read[Double].and((JsPath \ "long").read[Double])(Location.apply _)

implicit val placeReads: Reads[Place] =
  (JsPath \ "name").read[String].and((JsPath \ "location").read[Location])(Place.apply _)

Next we’ll define the Action.

def savePlace(): Action[JsValue] = Action(parse.json) { request =>
  val placeResult = request.body.validate[Place]
  placeResult.fold(
    errors => {
      BadRequest(Json.obj("message" -> JsError.toJson(errors)))
    },
    place => {
      Place.save(place)
      Ok(Json.obj("message" -> ("Place '" + place.name + "' saved.")))
    }
  )
}

This Action is more complicated than our list case. Some things to note:

Body parsers can be typed with a case class, an explicit Reads object or take a function. So we can offload even more of the work onto Play to make it automatically parse JSON to a case class and validate it before even calling our Action:

import play.api.libs.functional.syntax._
import play.api.libs.json._
import play.api.libs.json.Reads._

implicit val locationReads: Reads[Location] =
  (JsPath \ "lat")
    .read[Double](min(-90.0).keepAnd(max(90.0)))
    .and((JsPath \ "long").read[Double](min(-180.0).keepAnd(max(180.0))))(Location.apply _)

implicit val placeReads: Reads[Place] =
  (JsPath \ "name").read[String](minLength[String](2)).and((JsPath \ "location").read[Location])(Place.apply _)

// This helper parses and validates JSON using the implicit `placeReads`
// above, returning errors if the parsed json fails validation.
def validateJson[A: Reads] = parse.json.validate(
  _.validate[A].asEither.left.map(e => BadRequest(JsError.toJson(e)))
)

// if we don't care about validation we could replace `validateJson[Place]`
// with `BodyParsers.parse.json[Place]` to get an unvalidated case class
// in `request.body` instead.
def savePlaceConcise: Action[Place] = Action(validateJson[Place]) { request =>
  // `request.body` contains a fully validated `Place` instance.
  val place = request.body
  Place.save(place)
  Ok(Json.obj("message" -> ("Place '" + place.name + "' saved.")))
}

Finally we’ll add a route binding in conf/routes:

POST  /places               controllers.Application.savePlace

We’ll test this action with valid and invalid requests to verify our success and error flows.

Testing the action with a valid data:

curl --include
  --request POST
  --header "Content-type: application/json" 
  --data '{"name":"Nuthanger Farm","location":{"lat" : 51.244031,"long" : -1.263224}}' 
  http://localhost:9000/places

Response:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 57

{"message":"Place 'Nuthanger Farm' saved."}

Testing the action with a invalid data, missing “name” field:

curl --include
  --request POST
  --header "Content-type: application/json"
  --data '{"location":{"lat" : 51.244031,"long" : -1.263224}}' 
  http://localhost:9000/places

Response:

HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 79

{"message":{"obj.name":[{"msg":"error.path.missing","args":[]}]}}

Testing the action with a invalid data, wrong data type for “lat”:

curl --include
  --request POST
  --header "Content-type: application/json" 
  --data '{"name":"Nuthanger Farm","location":{"lat" : "xxx","long" : -1.263224}}' 
  http://localhost:9000/places

Response:

HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 92

{"message":{"obj.location.lat":[{"msg":"error.expected.jsnumber","args":[]}]}}

§Summary

Play is designed to support REST with JSON and developing these services should hopefully be straightforward. The bulk of the work is in writing Reads and Writes for your model, which is covered in detail in the next section.

Next: JSON Reads/Writes/Format Combinators