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

§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, using play.api.libs.functional.syntax, 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:

import play.api.libs.functional.syntax._ // Combinator syntax

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: Reads[Location] = 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 _)

§Functional combinators with Reads

Usual functional combinators are available, to transform and transform Reads instances or their results.

val strReads: Reads[String] = JsPath.read[String]

// .map
val intReads: Reads[Int] = strReads.map { str =>
  str.toInt
}
// e.g. reads JsString("123") as 123

// .flatMap
val objReads: Reads[JsObject] = strReads.flatMap { rawJson =>
  // consider something like { "foo": "{ \"stringified\": \"json\" }" }
  Reads { _ =>
    Json.parse(rawJson).validate[JsObject]
  }
}

// .collect
val boolReads1: Reads[Boolean] = strReads.collect(JsonValidationError("in.case.it.doesn-t.match")) {
  case "no" | "false" | "n" => false
  case _                    => true
}

// .orElse
val boolReads2: Reads[Boolean] = JsPath.read[Boolean].orElse(boolReads1)

// .andThen
val postprocessing: Reads[Boolean] = Reads[JsBoolean] {
  case JsString("no" | "false" | "n") =>
    JsSuccess(JsFalse)

  case _ => JsSuccess(JsTrue)
}.andThen(JsPath.read[Boolean])

The filter combinators can also be applied on Reads (see the next section for more validation).

val positiveIntReads = JsPath.read[Int].filter(_ > 0)
val smallIntReads    = positiveIntReads.filterNot(_ > 100)

val positiveIntReadsWithCustomErr = JsPath
  .read[Int]
  .filter(JsonValidationError("error.positive-int.expected"))(_ > 0)

Some specific combinators are available to process JSON before reading (contrary to .andThen combinator).

// .composeWith
val preprocessing1: Reads[Boolean] =
  JsPath
    .read[Boolean]
    .composeWith(Reads[JsBoolean] {
      case JsString("no" | "false" | "n") =>
        JsSuccess(JsFalse)

      case _ => JsSuccess(JsTrue)
    })

val preprocessing2: Reads[Boolean] = JsPath.read[Boolean].preprocess {
  case JsString("no" | "false" | "n") =>
    JsFalse

  case _ => JsTrue
}

§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 JsSuccess(nme, _) => println(s"Name: $nme")
  case e: JsError        => println(s"Errors: ${JsError.toJson(e)}")
}

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 JsSuccess(place, _) => {
    val _: Place = place
    // 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]
)(l => (l.lat, l.long))

implicit val residentWrites: Writes[Resident] = (
  (JsPath \ "name").write[String] and
    (JsPath \ "age").write[Int] and
    (JsPath \ "role").writeNullable[String]
)(r => (r.name, r.age, r.role))

implicit val placeWrites: Writes[Place] = (
  (JsPath \ "name").write[String] and
    (JsPath \ "location").write[Location] and
    (JsPath \ "residents").write[Seq[Resident]]
)(p => (p.name, p.location, p.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)

There are a few differences between complex Writes and Reads:

§Functional combinators with Writes

As for Reads, some functional combinators can be used on Writes instances, to adapt how to write values as JSON.

val plus10Writes: Writes[Int] = implicitly[Writes[Int]].contramap(_ + 10)

val doubleAsObj: Writes[Double] =
  implicitly[Writes[Double]].transform { js =>
    Json.obj("_double" -> js)
  }

val someWrites: Writes[Some[String]] =
  implicitly[Writes[Option[String]]].narrow[Some[String]]

§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.apply _)

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

§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]
)(l => (l.lat, l.long))

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, l => (l.lat, l.long))

Like Reads and Writes functional combinators are provided on Format.

val strFormat = implicitly[Format[String]]
val intFormat: Format[Int] =
  strFormat.bimap(_.size, List.fill(_: Int)('?').mkString)

Next: JSON automated mapping