§JSON basics
Modern web applications often need to parse and generate data in the JSON (JavaScript Object Notation) format. Play supports this via its JSON library.
JSON is a lightweight data-interchange format and looks like this:
{
"name" : "Watership Down",
"location" : {
"lat" : 51.235685,
"long" : -1.309197
},
"residents" : [ {
"name" : "Fiver",
"age" : 4,
"role" : null
}, {
"name" : "Bigwig",
"age" : 6,
"role" : "Owsla"
} ]
}
To learn more about JSON, see json.org.
§The Play JSON library
The play.api.libs.json
package contains data structures for representing JSON data
and utilities for converting between these data structures and other data representations. Types of interest are:
§JsValue
This is a trait representing any JSON value. The JSON library has a case class extending JsValue
to represent each valid JSON type:
Using the various JSValue types, you can construct a representation of any JSON structure.
§Json
The Json object provides utilities, primarily for conversion to and from JsValue structures.
§JsPath
Represents a path into a JSValue structure, analogous to XPath for XML. This is used for traversing JsValue structures and in patterns for implicit converters.
§Converting to a JsValue
§Using string parsing
import play.api.libs.json._
val json: JsValue = Json.parse("""
{
"name" : "Watership Down",
"location" : {
"lat" : 51.235685,
"long" : -1.309197
},
"residents" : [ {
"name" : "Fiver",
"age" : 4,
"role" : null
}, {
"name" : "Bigwig",
"age" : 6,
"role" : "Owsla"
} ]
}
""")
§Using class construction
import play.api.libs.json._
val json: JsValue = JsObject(Seq(
"name" -> JsString("Watership Down"),
"location" -> JsObject(Seq("lat" -> JsNumber(51.235685), "long" -> JsNumber(-1.309197))),
"residents" -> JsArray(Seq(
JsObject(Seq(
"name" -> JsString("Fiver"),
"age" -> JsNumber(4),
"role" -> JsNull
)),
JsObject(Seq(
"name" -> JsString("Bigwig"),
"age" -> JsNumber(6),
"role" -> JsString("Owsla")
))
))
))
Json.obj
and Json.arr
can simplify construction a bit. Note that most values don’t need to be explicitly wrapped by JsValue classes, the factory methods use implicit conversion (more on this below).
import play.api.libs.json.{JsNull,Json,JsString,JsValue}
val json: JsValue = Json.obj(
"name" -> "Watership Down",
"location" -> Json.obj("lat" -> 51.235685, "long" -> -1.309197),
"residents" -> Json.arr(
Json.obj(
"name" -> "Fiver",
"age" -> 4,
"role" -> JsNull
),
Json.obj(
"name" -> "Bigwig",
"age" -> 6,
"role" -> "Owsla"
)
)
)
§Using Writes converters
Scala to JsValue conversion is performed by the utility method Json.toJson[T](T)(implicit writes: Writes[T])
. This functionality depends on a converter of type Writes[T]
which can convert a T
to a JsValue
.
The Play JSON API provides implicit Writes
for most basic types, such as Int
, Double
, String
, and Boolean
. It also supports Writes
for collections of any type T
that a Writes[T]
exists.
import play.api.libs.json._
// basic types
val jsonString = Json.toJson("Fiver")
val jsonNumber = Json.toJson(4)
val jsonBoolean = Json.toJson(false)
// collections of basic types
val jsonArrayOfInts = Json.toJson(Seq(1, 2, 3, 4))
val jsonArrayOfStrings = Json.toJson(List("Fiver", "Bigwig"))
To convert your own models to JsValues, you must define implicit Writes
converters and provide them in scope.
case class Location(lat: Double, long: Double)
case class Resident(name: String, age: Int, role: Option[String])
case class Place(name: String, location: Location, residents: Seq[Resident])
import play.api.libs.json._
implicit val locationWrites = new Writes[Location] {
def writes(location: Location) = Json.obj(
"lat" -> location.lat,
"long" -> location.long
)
}
implicit val residentWrites = new Writes[Resident] {
def writes(resident: Resident) = Json.obj(
"name" -> resident.name,
"age" -> resident.age,
"role" -> resident.role
)
}
implicit val placeWrites = new Writes[Place] {
def writes(place: Place) = Json.obj(
"name" -> place.name,
"location" -> place.location,
"residents" -> place.residents)
}
val place = Place(
"Watership Down",
Location(51.235685, -1.309197),
Seq(
Resident("Fiver", 4, None),
Resident("Bigwig", 6, Some("Owsla"))
)
)
val json = Json.toJson(place)
Alternatively, you can define your Writes
using the combinator pattern:
Note: The combinator pattern is covered in detail in JSON Reads/Writes/Formats Combinators.
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val locationWrites: Writes[Location] = (
(JsPath \ "lat").write[Double] and
(JsPath \ "long").write[Double]
)(unlift(Location.unapply))
implicit val residentWrites: Writes[Resident] = (
(JsPath \ "name").write[String] and
(JsPath \ "age").write[Int] and
(JsPath \ "role").writeNullable[String]
)(unlift(Resident.unapply))
implicit val placeWrites: Writes[Place] = (
(JsPath \ "name").write[String] and
(JsPath \ "location").write[Location] and
(JsPath \ "residents").write[Seq[Resident]]
)(unlift(Place.unapply))
§Traversing a JsValue structure
You can traverse a JsValue
structure and extract specific values. The syntax and functionality is similar to Scala XML processing.
Note: The following examples are applied to the JsValue structure created in previous examples.
§Simple path \
Applying the \
operator to a JsValue
will return the property corresponding to the field argument, supposing this is a JsObject.
val lat = json \ "location" \ "lat"
// returns JsNumber(51.235685)
§Recursive path \\
Applying the \\
operator will do a lookup for the field in the current object and all descendants.
val names = json \\ "name"
// returns Seq(JsString("Watership Down"), JsString("Fiver"), JsString("Bigwig"))
§Index lookup (for JsArrays)
You can retrieve a value in a JsArray
using an apply operator with the index number.
val bigwig = (json \ "residents")(1)
// returns {"name":"Bigwig","age":6,"role":"Owsla"}
§Converting from a JsValue
§Using String utilities
Minified:
val minifiedString: String = Json.stringify(json)
{"name":"Watership Down","location":{"lat":51.235685,"long":-1.309197},"residents":[{"name":"Fiver","age":4,"role":null},{"name":"Bigwig","age":6,"role":"Owsla"}]}
Readable:
val readableString: String = Json.prettyPrint(json)
{
"name" : "Watership Down",
"location" : {
"lat" : 51.235685,
"long" : -1.309197
},
"residents" : [ {
"name" : "Fiver",
"age" : 4,
"role" : null
}, {
"name" : "Bigwig",
"age" : 6,
"role" : "Owsla"
} ]
}
§Using JsValue.as/asOpt
The simplest way to convert a JsValue
to another type is using JsValue.as[T](implicit fjs: Reads[T]): T
. This requires an implicit converter of type Reads[T]
to convert a JsValue
to T
(the inverse of Writes[T]
). As with Writes
, the JSON API provides Reads
for basic types.
val name = (json \ "name").as[String]
// "Watership Down"
val names = (json \\ "name").map(_.as[String])
// Seq("Watership Down", "Fiver", "Bigwig")
The as
method will throw a JsResultException
if the path is not found or the conversion is not possible. A safer method is JsValue.asOpt[T](implicit fjs: Reads[T]): Option[T]
.
val nameOption = (json \ "name").asOpt[String]
// Some("Watership Down")
val bogusOption = (json \ "bogus").asOpt[String]
// None
Although the asOpt
method is safer, any error information is lost.
§Using validation
The preferred way to convert from a JsValue
to another type is by using its validate
method (which takes an argument of type Reads
). This performs both validation and conversion, returning a type of JsResult
. JsResult
is implemented by two classes:
JsSuccess
- Represents a successful validation/conversion and wraps the result.JsError
- Represents unsuccessful validation/conversion and contains a list of validation errors.
You can apply various patterns for handling a validation result:
val json = { ... }
val nameResult: JsResult[String] = (json \ "name").validate[String]
// Pattern matching
nameResult match {
case s: JsSuccess[String] => println("Name: " + s.get)
case e: JsError => println("Errors: " + JsError.toFlatJson(e).toString())
}
// Fallback value
val nameOrFallback = nameResult.getOrElse("Undefined")
// map
val nameUpperResult: JsResult[String] = nameResult.map(_.toUpperCase())
// fold
val nameOption: Option[String] = nameResult.fold(
invalid = {
fieldErrors => fieldErrors.foreach(x => {
println("field: " + x._1 + ", errors: " + x._2)
})
None
},
valid = {
name => Some(name)
}
)
§JsValue to a model
To convert from JsValue to a model, you must define implicit Reads[T]
where T
is the type of your model.
Note: The pattern used to implement
Reads
and custom validation are covered in detail in JSON Reads/Writes/Formats Combinators.
case class Location(lat: Double, long: Double)
case class Resident(name: String, age: Int, role: Option[String])
case class Place(name: String, location: Location, residents: Seq[Resident])
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val locationReads: Reads[Location] = (
(JsPath \ "lat").read[Double] and
(JsPath \ "long").read[Double]
)(Location.apply _)
implicit val residentReads: Reads[Resident] = (
(JsPath \ "name").read[String] and
(JsPath \ "age").read[Int] and
(JsPath \ "role").readNullable[String]
)(Resident.apply _)
implicit val placeReads: Reads[Place] = (
(JsPath \ "name").read[String] and
(JsPath \ "location").read[Location] and
(JsPath \ "residents").read[Seq[Resident]]
)(Place.apply _)
val json = { ... }
val placeResult: JsResult[Place] = json.validate[Place]
// JsSuccess(Place(...),)
val residentResult: JsResult[Resident] = (json \ "residents")(1).validate[Resident]
// JsSuccess(Resident(Bigwig,6,Some(Owsla)),)
Next: JSON with HTTP