Documentation

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

§JSON automated mapping

If the JSON maps directly to a class, we provide a handy macro so that you don’t have to write the Reads[T], Writes[T], or Format[T] manually. Given the following case class:

case class Resident(name: String, age: Int, role: Option[String])

The following macro will create a Reads[Resident] based on its structure and the name of its fields:

import play.api.libs.json._

implicit val residentReads = Json.reads[Resident]

When compiling, the macro will inspect the given class and
inject the following code, exactly as if you had written it manually:

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

implicit val residentReads = (
  (__ \ "name").read[String] and
    (__ \ "age").read[Int] and
    (__ \ "role").readNullable[String]
)(Resident)

This is done at compile-time, so you don’t lose any type safety or performance.
Similar macros exists for a Writes[T] or a Format[T] :

import play.api.libs.json._

implicit val residentWrites = Json.writes[Resident]
import play.api.libs.json._

implicit val residentFormat = Json.format[Resident]

So, a complete example of performing automated conversion of a case class to JSON is as follows:

import play.api.libs.json._

implicit val residentWrites = Json.writes[Resident]

val resident = Resident(name = "Fiver", age = 4, role = None)

val residentJson: JsValue = Json.toJson(resident)

And a complete example of automatically parsing JSON to a case class is:

import play.api.libs.json._

implicit val residentReads = Json.reads[Resident]

// In a request, a JsValue is likely to come from `request.body.asJson`
// or just `request.body` if using the `Action(parse.json)` body parser
val jsonString: JsValue = Json.parse(
  """{
  "name" : "Fiver",
  "age" : 4
}"""
)

val residentFromJson: JsResult[Resident] =
  Json.fromJson[Resident](jsonString)

residentFromJson match {
  case JsSuccess(r: Resident, path: JsPath) =>
    println("Name: " + r.name)

  case e @ JsError(_) =>
    println("Errors: " + JsError.toJson(e).toString())
}

The value classes are also supported. Given the following value class, based on a String value:

final class IdText(val value: String) extends AnyVal

Then it’s also possible to generate a Reads[IdText] using the following macro (as String is already supported):

import play.api.libs.json._

implicit val idTextReads = Json.valueReads[IdText]

As for case classes, similar macros exists for a Writes[T] or a Format[T]:

import play.api.libs.json._

implicit val idTextWrites = Json.valueWrites[IdText]
import play.api.libs.json._

implicit val idTextFormat = Json.valueFormat[IdText]

Note: To be able to access JSON from request.body.asJson, the request must have a Content-Type header of application/json. You can relax this constraint by using the `tolerantJson` body parser.

The above example can be made even more concise by using body parsers with a typed validation function. See the savePlaceConcise example in the JSON with HTTP documentation.

§Requirements

The macros work for classes and traits meeting the following requirements.

Case classes automatically meet these requirements. For custom classes or traits, you might have to implement them.

A trait can also supported, if and only if it’s a sealed one and if the sub-types comply with the previous requirements:

sealed trait Role
case object Admin extends Role
class Contributor(val organization: String) extends Role {
  override def equals(obj: Any): Boolean = obj match {
    case other: Contributor if obj != null => this.organization == other.organization
    case _                                 => false
  }
}
object Contributor {
  def apply(organization: String): Contributor            = new Contributor(organization)
  def unapply(contributor: Contributor): Option[(String)] = Some(contributor.organization)
}

The JSON representation for instances of a sealed family includes a discriminator field, which specify the effective sub-type (a text field, with default name _type).

val adminJson = Json.parse("""
  { "_type": "scalaguide.json.ScalaJsonAutomatedSpec.Admin" }
""")

val contributorJson = Json.parse("""
  {
    "_type":"scalaguide.json.ScalaJsonAutomatedSpec.Contributor",
    "organization":"Foo"
  }
""")

// Each JSON objects is marked with the _type,
// indicating the fully-qualified name of sub-type

Then the macros are able generate Reads[T], OWrites[T] or OFormat[T].

import play.api.libs.json._

// First provide instance for each sub-types 'Admin' and 'Contributor':
implicit val adminFormat = OFormat[Admin.type](Reads[Admin.type] {
  case JsObject(_) => JsSuccess(Admin)
  case _           => JsError("Empty object expected")
}, OWrites[Admin.type] { _ =>
  Json.obj()
})

implicit val contributorFormat = Json.format[Contributor]

// Finally able to generate format for the sealed family 'Role'
implicit val roleFormat: OFormat[Role] = Json.format[Role]

§Custom Naming Strategies

To use a custom Naming Strategy you need to define a implicit JsonConfiguration object and a JsonNaming.

Two naming strategies are provided: the default one, using as-is the names of the class properties,
and the JsonNaming.SnakeCase case one.

A strategy other than the default one can be used as following:

import play.api.libs.json._

implicit val config = JsonConfiguration(SnakeCase)

implicit val userReads: Reads[PlayUser] = Json.reads[PlayUser]
import play.api.libs.json._

implicit val config = JsonConfiguration(SnakeCase)

implicit val userWrites: OWrites[PlayUser] = Json.writes[PlayUser]
import play.api.libs.json._

implicit val config = JsonConfiguration(SnakeCase)

implicit val userFormat: OFormat[PlayUser] = Json.format[PlayUser]

The trait representation can also be configured, with a custom name for the discriminator field or the way the names of the sub-types are encoded as value for this field:

val adminJson = Json.parse("""
  { "admTpe": "admin" }
""")

val contributorJson = Json.parse("""
  {
    "admTpe":"contributor",
    "organization":"Foo"
  }
""")

To do so, the settings discriminator and typeNaming can be defined in the resolved JsonConfiguration:

import play.api.libs.json._

implicit val cfg = JsonConfiguration(
  // Each JSON objects is marked with the admTpe, ...
  discriminator = "admTpe",
  // ... indicating the lower-cased name of sub-type
  typeNaming = JsonNaming { fullName =>
    fullName.drop(39 /* remove pkg */ ).toLowerCase
  }
)

// First provide instance for each sub-types 'Admin' and 'Contributor':
implicit val adminFormat = OFormat[Admin.type](Reads[Admin.type] {
  case JsObject(_) => JsSuccess(Admin)
  case _           => JsError("Empty object expected")
}, OWrites[Admin.type] { _ =>
  Json.obj()
})

implicit val contributorFormat = Json.format[Contributor]

// Finally able to generate format for the sealed family 'Role'
implicit val roleFormat: OFormat[Role] = Json.format[Role]

§Implementing your own Naming Strategy

To implement your own Naming Strategy you just need to implement the JsonNaming trait:

import play.api.libs.json._

object Lightbend extends JsonNaming {
  override def apply(property: String): String = s"lightbend_$property"
}

implicit val config = JsonConfiguration(Lightbend)

implicit val customWrites: OFormat[PlayUser] = Json.format[PlayUser]

§Customize the macro to output null

The macro can be configured to output null values in the Json instead of removing the empty fields:

import play.api.libs.json._

implicit val config         = JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)
implicit val residentWrites = Json.writes[Resident]

Next: JSON Transformers