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