Documentation

§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: Reads[Resident] = 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: Reads[Resident] = (
  (__ \ "name").read[String] and
    (__ \ "age").read[Int] and
    (__ \ "role").readNullable[String]
)(Resident.apply _)

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: OWrites[Resident] = Json.writes[Resident]
import play.api.libs.json._

implicit val residentFormat: Format[Resident] = 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: OWrites[Resident] = 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: Reads[Resident] = 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: Reads[IdText] = 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: Writes[IdText] = Json.valueWrites[IdText]
import play.api.libs.json._

implicit val idTextFormat: Format[IdText] = 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.

Class in Scala 2.x:

Class in Scala 3.1.x: (+3.1.2-RC2)

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
case class Contributor(organization: String) extends Role

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: OFormat[Contributor] = 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 = JsonConfiguration(SnakeCase)

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

implicit val config: JsonConfiguration = JsonConfiguration(SnakeCase)

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

implicit val config: JsonConfiguration = 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 = 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: OFormat[Contributor] = 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 OpenCollective extends JsonNaming {
  override def apply(property: String): String = s"opencollective_$property"
}

implicit val config: JsonConfiguration = JsonConfiguration(OpenCollective)

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         = JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)
implicit val residentWrites: OWrites[Resident] = Json.writes[Resident]

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. Have questions or advice to share? Go to our community forums to start a conversation with the community.