§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.")))
}
)
}
這個 動作
比我們的清單案例更複雜。一些注意事項
- 這個
動作
會預期一個要求,其Content-Type
標頭為text/json
或application/json
,以及包含要建立的實體的 JSON 表示的主體。 - 它使用特定於 JSON 的
BodyParser
,它會 剖析要求,並提供request.body
作為JsValue
。 - 我們使用
validate
方法進行轉換,它會依賴於我們的隱含Reads[Place]
。 - 若要處理驗證結果,我們使用具有錯誤和成功流程的
fold
。此模式可能很熟悉,因為它也用於 表單提交。 Action
也會傳送 JSON 回應。
主體剖析器可以使用案例類別、明確的 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,開發這些服務應當很直接。大部分工作在於為您的模型撰寫 Reads
和 Writes
,這會在下一個區段中詳細說明。
在此文件檔中發現錯誤?此頁面的原始程式碼可以在 此處 找到。在閱讀 文件檔指南 後,請隨時貢獻一個 pull request。有問題或建議要分享嗎?請前往 我們的社群論壇,與社群展開對話。