§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 aContent-Type
header ofapplication/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:
- It must have a companion object having
apply
andunapply
methods. - The return types of the
unapply
must match the argument types of theapply
method. - The parameter names of the
apply
method must be the same as the property names desired in the JSON.
Class in Scala 3.1.x: (+3.1.2-RC2)
- It must be provided a
Conversion
to a_ <: Product
. - It must be provided a valid
ProductOf
.
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 Lightbend extends JsonNaming {
override def apply(property: String): String = s"lightbend_$property"
}
implicit val config: JsonConfiguration = 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 = JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)
implicit val residentWrites: OWrites[Resident] = Json.writes[Resident]
Next: JSON Transformers