Documentation

JSON Reads/Writes/Format Combinators

JSON basics introduced Reads and Writes converters which are used to convert between JsValue structures and other data types. This page covers in greater detail how to build these converters and how to use validation during conversion.

The examples on this page will use this JsValue structure and corresponding model:

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"
  } ]
}
""")
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])

JsPath

JsPath is a core building block for creating Reads/Writes. JsPath represents the location of data in a JsValue structure. You can use the JsPath object (root path) to define a JsPath child instance by using syntax similar to traversing JsValue:

import play.api.libs.json._

val json = { ... }

// Simple path
val latPath = JsPath \ "location" \ "lat"

// Recursive path
val namesPath = JsPath \\ "name"

// Indexed path
val firstResidentPath = (JsPath \ "residents")(0)

The play.api.libs.json package defines an alias for JsPath: __ (double underscore). You can use this if you prefer:

val longPath = __ \ "location" \ "long"

Reads

Reads converters are used to convert from a JsValue to another type. You can combine and nest Reads to create more complex Reads.

You will require these imports to create Reads:

import play.api.libs.json._ // JSON library
import play.api.libs.json.Reads._ // Custom validation helpers
import play.api.libs.functional.syntax._ // Combinator syntax

Path Reads

JsPath has methods to create special Reads that apply another Reads to a JsValue at a specified path:

Note: The JSON library provides implicit Reads for basic types such as String, Int, Double, etc.

Defining an individual path Reads looks like this:

val nameReads: Reads[String] = (JsPath \ "name").read[String]

Complex Reads

You can combine individual path Reads to form more complex Reads which can be used to convert to complex models.

For easier understanding, we’ll break down the combine functionality into two statements. First combine Reads objects using the and combinator:

val locationReadsBuilder = 
  (JsPath \ "lat").read[Double] and 
  (JsPath \ "long").read[Double]

This will yield a type of FunctionalBuilder[Reads]#CanBuild2[Double, Double]. This is an intermediary object and you don’t need to worry too much about it, just know that it’s used to create a complex Reads.

Second call the apply method of CanBuildX with a function to translate individual values to your model, this will return your complex Reads. If you have a case class with a matching constructor signature, you can just use its apply method:

implicit val locationReads = locationReadsBuilder.apply(Location.apply _)

Here’s the same code in a single statement:

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

Validation with Reads

The JsValue.validate method was introduced in JSON basics as the preferred way to perform validation and conversion from a JsValue to another type. Here’s the basic pattern:

val json = { ... }

val nameReads: Reads[String] = (JsPath \ "name").read[String]

val nameResult: JsResult[String] = json.validate[String](nameReads)

nameResult match {
  case s: JsSuccess[String] => println("Name: " + s.get)
  case e: JsError => println("Errors: " + JsError.toFlatJson(e).toString()) 
}

Default validation for Reads is minimal, such as checking for type conversion errors. You can define custom validation rules by using Reads validation helpers. Here are some that are commonly used:

To add validation, apply helpers as arguments to the JsPath.read method:

val improvedNameReads = 
  (JsPath \ "name").read[String](minLength[String](2))

Putting it all together

By using complex Reads and custom validation we can define a set of effective Reads for our example model and apply them:

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

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 residentReads: Reads[Resident] = (
  (JsPath \ "name").read[String](minLength[String](2)) and
  (JsPath \ "age").read[Int](min(0) keepAnd max(150)) and
  (JsPath \ "role").readNullable[String]
)(Resident.apply _)

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


val json = { ... }

json.validate[Place] match {
  case s: JsSuccess[Place] => {
    val place: Place = s.get
    // do something with place
  }
  case e: JsError => {
    // error handling flow
  }
}

Note that complex Reads can be nested. In this case, placeReads uses the previously defined implicit locationReads and residentReads at specific paths in the structure.

Writes

Writes converters are used to convert from some type to a JsValue.

You can build complex Writes using JsPath and combinators very similar to Reads. Here’s the Writes for our example model:

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))


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)

There are a few differences between complex Writes and Reads:

Recursive Types

One special case that our example model doesn’t demonstrate is how to handle Reads and Writes for recursive types. JsPath provides lazyRead and lazyWrite methods that take call-by-name parameters to handle this:

case class User(name: String, friends: Seq[User])

implicit lazy val userReads: Reads[User] = (
  (__ \ "name").read[String] and
  (__ \ "friends").lazyRead(Reads.seq[User](userReads))
)(User)

implicit lazy val userWrites: Writes[User] = (
  (__ \ "name").write[String] and
  (__ \ "friends").lazyWrite(Writes.seq[User](userWrites))
)(unlift(User.unapply))

Format

Format[T] is just a mix of the Reads and Writes traits and can be used for implicit conversion in place of its components.

Creating Format from Reads and Writes

You can define a Format by constructing it from Reads and Writes of the same type:

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 _)

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

implicit val locationFormat: Format[Location] =
  Format(locationReads, locationWrites)

Creating Format using combinators

In the case where your Reads and Writes are symmetrical (which may not be the case in real applications), you can define a Format directly from combinators:

implicit val locationFormat: Format[Location] = (
  (JsPath \ "lat").format[Double](min(-90.0) keepAnd max(90.0)) and
  (JsPath \ "long").format[Double](min(-180.0) keepAnd max(180.0))
)(Location.apply, unlift(Location.unapply))

Next: JSON Transformers


Found an error in this documentation? The source code for this page can be found here. After reading the documentation guidelines, please feel free to contribute a pull request.