case class Product(ean: Long, name: String, description: String)
import play.api.libs.json._
implicit object ProductWrites extends Writes[Product] {
def writes(p: Product) = Json.obj(
"ean" -> Json.toJson(p.ean),
"name" -> Json.toJson(p.name),
"description" -> Json.toJson(p.description)
)
}
// 更简介的方法
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val productWrites: Writes[Product] = (
(JsPath \ "ean").write[Long] and
(JsPath \ "name").write[String] and
(JsPath \ "description").write[String]
)(unlift(Product.unapply))
// we can use a helper function that defines a default case
// class formatter at runtime, which gives
// the same output as the previous example:
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val productWrites = Json.writes[Product]
Writes[Product] for the administrative interface
1
2
3
4
5
6
7
8
9
10
import play.api.libs.json._
import play.api.libs.functional.syntax._
val adminProductWrites: Writes[Product] = (
(JsPath \ "ean").write[Long] and
(JsPath \ "name").write[String] and
(JsPath \ "description").write[String] and
(JsPath \ "price").write[BigDecimal]
)(unlift(Product.unapply))
val json = Json.toJson(product)(AdminProductWrites)
Often, you don’t even need to manually perform this step.
1
2
3
4
5
6
7
8
def postProduct() = Action { request =>
val jsValueOption = request.body.asJson
jsValueOption.map { json =>
// Do something with the JSON
}.getOrElse {
// Not a JSON body
}
}
If you’re only willing to accept JSON for an action, which is common,
you can use the parse.json body parser:
1
2
3
4
5
// it’ll returnan HTTP status of400 Bad Request ifthe content type is wrong.
def postProduct2() = Action(parse.json) { request =>
val jsValue = request.body
// Do something with the JSON
}
Sometimes you have to deal with misbehaving clients that send JSON without
proper Content-Type headers. In that case, you can use the parse.tolerantJson
body parser, which doesn’t check the header, but just tries to parse the body as JSON.
JsValue has the as[T] and asOpt[T] methods,
1
2
3
4
5
6
7
8
9
val jsValue = JsString("Johnny")
val name = jsValue.as[String]
val age = jsValue.as[Int] // Throws play.api.libs.json.JsResultExceptionval age: Option[Int] = jsValue.asOpt[Int] // == Noneval name: Option[String] = jsValue.asOpt[String] // == Some("Johnny")val age = jsValue.validate[Int] // == JsErrorval name = jsValue.validate[String] // == JsSuccess(Johnny,)
Of course, often you’ll be dealing with more complex JSON structures. There are
three methods for traversing a JsValue tree:
\—Selects an element in a JsObject, returning a JsValue
\\—Selects an element in the entire tree, returning a Seq[JsValue]
apply—Selects an element in a JsArray , returning a JsValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Json._
val json: JsValue = toJson(Map(
"name" -> toJson("Johnny"),
"age" -> toJson(42),
"tags" -> toJson(List("constructor", "builder")),
"company" -> toJson(Map(
"name" -> toJson("Constructors Inc.")))))
val name = (json \ "name").as[Strnig]
val age = (json \ "age").asOpt[Int]
val companyName = (json \ "company" \ "name").as[String]
val firstTag = (json \ "tags")(0).as[String]
val allNames = (json \\ "name").map(_as[String])
(json \ "name") match {
case JsString(name) => println(name)
case JsUndefined(error) => println(error)
case _ => println("Invalid type!")
}
Reusable consumers
The Reads[T] trait has a single method, reads(json: JsValue): JsResult[T],
which deserializes JSON into a JsSuccess that wraps an object of type T or a JsError
that gives you access to JSON parsing errors, following the pattern of Scala’s
Either[Error, T].
{
"obj.manufacturer.contact_details.email" : [
{ "msg" : "validate.error.email", "args" : [] }
],
"obj.name" : [
{"msg" : "validate.error.minlength", "args" : [5] }
],
"obj.tags" : [
{"msg" : "validate.error.missing-path", "args" : [] }
]
}
// You may requires errors in a particular simplified JSON format
[
{
"path" : "/manufacturer/contact_details/email",
"errors": ["validate.error.email"]
},
{ "path" : "/name", "errors" : ["validate.error.minlength"] },
{ "path" : "/tags", "errors" : ["validate.error.missing-path"] }
]
// format a path as a string
implicit val JsPathWrites =
Writes[JsPath](p => JsString(p.toString))
// format an error as a string
implicit val ValicateionErrorWrites =
Writes[ValidationError](e => JsString(e.message))
implicit val jsonValidateErrorWrites = (
(JsPath \ "path").write[JsPath] and
(JsPath \ "errors").write[Seq[ValidationError]]
tupled
)
One such library is Jerkson; it’s possible to use Jerkson directly, or
you can use any other JSON library that you like.
Authenticating JSON web service requests
Authentication means identifying the “user” who’s sending the request,
by requiring and checking valid credentials, usually username and password.
In a conventional web application, authentication is usually implemented by using
an HTML login form to submit credentials to a server application, which then
maintains a session state that future requests from the same user are
associated with. In our JSON web service architecture, there are no HTML forms,
so we use different methods to associate authentication credentials with requests.
Adding authentication to action methods
The simplest approach is to perform authentication for every HTTP request, before
returning the usual response or an HTTP error that indicates that the client isn’t
authorized to access the requested resource.
// Helper function to extract credentials from a request query string
defreadQueryString(request: Request[_]):
Option[Either[Result, (String, String)]] = {
request.queryString.get("user").map{ user =>
request.queryStirng.get("password").map { password =>
Right((user.head, password.head))
}.getOrElse {
Left(BadRequest("Password not specified"))
}
}
}
// Updated action helper that extracts credentials before authentication
defAuthenticatedAction(f: Request[AnyContent] => Result):
Action[AnyContent] = {
Action { request =>
val maybeCredentials = readQueryString(request)
maybeCredentials.map { resultOrCredentials =>
resultOrCredentials match {
case Left(errorResult) => errorResult
case Right(credentials) => {
val (user, password) = credentials
if (authenticate(user, password)) {
f(request)
} else {
Unauthorized("Invalid user name or password")
}
}
}
}.getOrElse {
Unauthorized("No user name and password provided")
}
}
}
Using basic authentication
A more standard way to send authentication credentials with an HTTP request is to use
HTTP basic authentication, which sends credentials in an HTTP header.
A server requests basic authentication by sending an HTTP 401 Unauthorized
response with an additional WWW-Authenticate header. The header has a value like
Basic realm="Product catalog". This specifies the required authentication type
and names the protected resource.
def readBasicAuthentication(headers: Headers):
Option[Either[Result, (String, String)]] = {
headers.get(Http.HeaderNames.AUTHORIZATION).map { header =>
val BasicHeader = "Basic (.*)".r
header match {
case BasicHeader(base64) => {
try {
import org.apache.commons.codec.binary.base64
val decodedBytes = base64.decodeBase64(base64.getBytes)
val credentials = new String(decodedBytes).split(":", 2)
credentials match {
case Array(username, password) =>
Right(username -> password)
case _ => Left("Invalid basic authentication")
}
}
}
case _ => Left(BadRequest("Bad Authorization header"))
}
}
}
val maybeCredentials = readQueryString(request) orElse
readBasicAuthentication(request.headers)
// curl --include --user peter:secret http://localhost:9000/
Other authentication methods
Web services often use one of two alternatives:
Token-based authentication—Providing a signed API key that clients can send with
requests, either in a custom HTTP header or query string parameter
Session-based authentication—Using one method to authenticate, and then
providing a session identifier that clients can send, either in an HTTP cookie or an
HTTP header