文件

§JSON 與 HTTP

Play 支援內容類型為 JSON 的 HTTP 要求和回應,方法是將 HTTP API 與 JSON 函式庫結合使用。

有關控制器、動作和路由的詳細資訊,請參閱 HTTP 編程

我們將透過設計一個簡單的 RESTful 網路服務來示範必要的概念,以 GET 一個實體清單,並接受 POST 來建立新的實體。此服務將對所有資料使用 JSON 的內容類型。

以下是我們將用於服務的模型

case class Location(lat: Double, long: Double)
object Location {
  def unapply(l: Location): Option[(Double, Double)] = Some(l.lat, l.long)
}

case class Place(name: String, location: Location)

object Place {
  var list: List[Place] = {
    List(
      Place(
        "Sandleford",
        Location(51.377797, -1.318965)
      ),
      Place(
        "Watership Down",
        Location(51.235685, -1.309197)
      )
    )
  }

  def save(place: Place): Unit = {
    list = list ::: List(place)
  }

  def unapply(p: Place): Option[(String, Location)] = Some(p.name, p.location)
}

§以 JSON 提供實體清單

我們將從將必要的匯入新增到我們的控制器開始。

import play.api.mvc._

class HomeController @Inject() (cc: ControllerComponents) extends AbstractController(cc) {}

在撰寫我們的 動作之前,我們需要進行將我們的模型轉換為 JsValue 表示的管道作業。這可透過定義一個隱含的 Writes[Place] 來完成。

implicit val locationWrites: Writes[Location] =
  (JsPath \ "lat").write[Double].and((JsPath \ "long").write[Double])(unlift(Location.unapply))

implicit val placeWrites: Writes[Place] =
  (JsPath \ "name").write[String].and((JsPath \ "location").write[Location])(unlift(Place.unapply))

接下來,我們撰寫我們的 動作

def listPlaces() = Action {
  val json = Json.toJson(Place.list)
  Ok(json)
}

動作會擷取 Place 物件的清單,使用 Json.toJson 和我們的隱含 Writes[Place] 將它們轉換為 JsValue,並將其作為結果的主體傳回。Play 會將結果辨識為 JSON,並設定回應的適當 Content-Type 標頭和主體值。

最後一個步驟是在 conf/routes 中為我們的 動作新增一個路由

GET   /places               controllers.Application.listPlaces

我們可以透過使用瀏覽器或 HTTP 工具提出要求來測試動作。此範例使用 unix 命令列工具 cURL

curl --include https://127.0.0.1:9000/places

回應

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 141

[{"name":"Sandleford","location":{"lat":51.377797,"long":-1.318965}},{"name":"Watership Down","location":{"lat":51.235685,"long":-1.309197}}]

§在 JSON 中建立新的實體執行個體

對於此 動作,我們需要定義一個隱含的 Reads[Place] 來將 JsValue 轉換為我們的模型。

implicit val locationReads: Reads[Location] =
  (JsPath \ "lat").read[Double].and((JsPath \ "long").read[Double])(Location.apply _)

implicit val placeReads: Reads[Place] =
  (JsPath \ "name").read[String].and((JsPath \ "location").read[Location])(Place.apply _)

接下來,我們將定義 動作

def savePlace(): Action[JsValue] = Action(parse.json) { request =>
  val placeResult = request.body.validate[Place]
  placeResult.fold(
    errors => {
      BadRequest(Json.obj("message" -> JsError.toJson(errors)))
    },
    place => {
      Place.save(place)
      Ok(Json.obj("message" -> ("Place '" + place.name + "' saved.")))
    }
  )
}

這個 動作比我們的清單案例更複雜。一些注意事項

主體剖析器可以使用案例類別、明確的 Reads 物件進行輸入,或採用函式。因此,我們可以將更多工作卸載給 Play,讓它自動將 JSON 剖析為案例類別,並在呼叫我們的 Action 之前 驗證 它。

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

implicit val locationReads: Reads[Location] =
  (JsPath \ "lat")
    .read[Double](min(-90.0).keepAnd(max(90.0)))
    .and((JsPath \ "long").read[Double](min(-180.0).keepAnd(max(180.0))))(Location.apply _)

implicit val placeReads: Reads[Place] =
  (JsPath \ "name").read[String](minLength[String](2)).and((JsPath \ "location").read[Location])(Place.apply _)

// This helper parses and validates JSON using the implicit `placeReads`
// above, returning errors if the parsed json fails validation.
def validateJson[A: Reads] = parse.json.validate(
  _.validate[A].asEither.left.map(e => BadRequest(JsError.toJson(e)))
)

// if we don't care about validation we could replace `validateJson[Place]`
// with `BodyParsers.parse.json[Place]` to get an unvalidated case class
// in `request.body` instead.
def savePlaceConcise: Action[Place] = Action(validateJson[Place]) { request =>
  // `request.body` contains a fully validated `Place` instance.
  val place = request.body
  Place.save(place)
  Ok(Json.obj("message" -> ("Place '" + place.name + "' saved.")))
}

最後,我們會在 conf/routes 中新增路由繫結。

POST  /places               controllers.Application.savePlace

我們會使用有效和無效的要求來測試此動作,以驗證我們的成功和錯誤流程。

使用有效資料測試動作

curl --include
  --request POST
  --header "Content-type: application/json" 
  --data '{"name":"Nuthanger Farm","location":{"lat" : 51.244031,"long" : -1.263224}}' 
  https://127.0.0.1:9000/places

回應

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 57

{"message":"Place 'Nuthanger Farm' saved."}

使用無效資料測試動作,缺少「名稱」欄位

curl --include
  --request POST
  --header "Content-type: application/json"
  --data '{"location":{"lat" : 51.244031,"long" : -1.263224}}' 
  https://127.0.0.1:9000/places

回應

HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 79

{"message":{"obj.name":[{"msg":"error.path.missing","args":[]}]}}

使用無效資料測試動作,「lat」的資料類型錯誤

curl --include
  --request POST
  --header "Content-type: application/json" 
  --data '{"name":"Nuthanger Farm","location":{"lat" : "xxx","long" : -1.263224}}' 
  https://127.0.0.1:9000/places

回應

HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 92

{"message":{"obj.location.lat":[{"msg":"error.expected.jsnumber","args":[]}]}}

§摘要

Play 設計用於支援使用 JSON 的 REST,開發這些服務應當很直接。大部分工作在於為您的模型撰寫 ReadsWrites,這會在下一個區段中詳細說明。

下一步:JSON Reads/Writes/Format 組合器


在此文件檔中發現錯誤?此頁面的原始程式碼可以在 此處 找到。在閱讀 文件檔指南 後,請隨時貢獻一個 pull request。有問題或建議要分享嗎?請前往 我們的社群論壇,與社群展開對話。