§HTTPとJSON
Playは、JSONライブラリと組み合わせてHTTP APIを使用することにより、JSONのコンテンツタイプを持つHTTPリクエストとレスポンスをサポートします。
コントローラ、アクション、ルーティングの詳細については、HTTPプログラミング を参照してください。
エンティティのリストを取得するGET、および新しいエンティティを作成するためのPOSTを受け入れる簡単なRESTful Webサービスを設計することで、必要な概念を説明します。サービスはすべてのデータに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) {}
Action
を記述する前に、モデルから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))
次に、Action
を記述します。
def listPlaces() = Action {
val json = Json.toJson(Place.list)
Ok(json)
}
Action
は、Place
オブジェクトのリストを取得し、暗黙的なWrites[Place]
を使用してJson.toJson
でJsValue
に変換し、これを結果の本文として返します。Playは結果をJSONとして認識し、適切なContent-Type
ヘッダーとレスポンスの本文値を設定します。
最後の手順は、conf/routes
にAction
のルートを追加することです。
GET /places controllers.Application.listPlaces
ブラウザまたはHTTPツールでリクエストを作成することで、アクションをテストできます。この例では、unixコマンドラインツールcURLを使用しています。
curl --include http://localhost: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で新しいエンティティインスタンスを作成する
このAction
では、JsValue
をモデルに変換するために、暗黙的なReads[Place]
を定義する必要があります。
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 _)
次に、Action
を定義します。
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.")))
}
)
}
このAction
は、リストの場合よりも複雑です。注意すべき点がいくつかあります。
- この
Action
は、text/json
またはapplication/json
のContent-Type
ヘッダーと、作成するエンティティのJSON表現を含む本文を持つリクエストを予期します。 - JSON固有の
BodyParser
を使用します。これにより、リクエストが解析され、request.body
がJsValue
として提供されます。 - 暗黙的な
Reads[Place]
に依存する変換にvalidate
メソッドを使用しました。 - バリデーション結果を処理するために、エラーと成功のフローで
fold
を使用しました。このパターンは、フォーム送信にも使用されるため、なじみがあるかもしれません。 Action
はJSONレスポンスも送信します。
ボディパーサーは、ケースクラス、明示的なReads
オブジェクトで型指定するか、関数を使用できます。そのため、JSONをケースクラスに自動的に解析し、Action
を呼び出す前に検証するように、Playにさらに多くの作業をオフロードできます。
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}}'
http://localhost:9000/places
レスポンス
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 57
{"message":"Place 'Nuthanger Farm' saved."}
無効なデータでアクションをテストする、「name」フィールドがありません
curl --include
--request POST
--header "Content-type: application/json"
--data '{"location":{"lat" : 51.244031,"long" : -1.263224}}'
http://localhost: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}}'
http://localhost: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
の記述にあり、これについては次のセクションで詳しく説明します。
このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントのガイドラインをお読みになった後、プルリクエストを送信してください。質問や共有するアドバイスがありますか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。