§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. Some of the features of this package are:
- Automatic conversion to and from case classes with minimal boilerplate. If you want to get up and running quickly with minimal code, this is probably the place to start.
- Custom validation while parsing.
- Automatic parsing of JSON in request bodies, with auto-generated errors if content isn’t parseable or incorrect Content-type headers are supplied.
- Can be used outside of a Play application as a standalone library. Just add
libraryDependencies += "com.typesafe.play" %% "play-json" % playVersion
to yourbuild.sbt
file. - Highly customizable.
The package provides the following types:
§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(
IndexedSeq(
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
import play.api.libs.json.Json
import play.api.libs.json.JsString
import play.api.libs.json.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 JsValue
s, 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 in a JsObject
, or the item at that index in a JsArray
val lat = (json \ "location" \ "lat").get
// returns JsNumber(51.235685)
val bigwig = (json \ "residents" \ 1).get
// returns {"name":"Bigwig","age":6,"role":"Owsla"}
The \
operator returns a JsLookupResult
, which is either JsDefined
or JsUndefined
. You can chain multiple \
operators, and the result will be JsUndefined
if any intermediate value cannot be found. Calling get
on a JsLookupResult
attempts to get the value if it is defined and throws an exception if it is not.
You can also use the Direct lookup apply
method (below) to get a field in an object or index in an array. Like get
, this method will throw an exception if the value does not exist.
§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"))
§Direct lookup
You can retrieve a value in a JsArray
or JsObject
using an .apply
operator, which is identical to the Simple path \
operator except it returns the value directly (rather than wrapping it in a JsLookupResult
) and throws an exception if the index or key is not found:
val name = json("name")
// returns JsString("Watership Down")
val bigwig2 = json("residents")(1)
// returns {"name":"Bigwig","age":6,"role":"Owsla"}
// (json("residents")(3)
// throws an IndexOutOfBoundsException
// json("bogus")
// throws a NoSuchElementException
This is useful if you are writing quick-and-dirty code and are accessing some JSON values you know to exist, for example in one-off scripts or in the REPL.
§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 JsSuccess(name, _) => println(s"Name: $name")
case e: JsError => println(s"Errors: ${JsError.toJson(e)}")
}
// 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(s"field: ${x._1}, errors: ${x._2}")
}
Option.empty[String]
},
valid = Some(_)
)
§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