ドキュメント

§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.toJsonJsValueに変換し、これを結果の本文として返します。Playは結果をJSONとして認識し、適切なContent-Typeヘッダーとレスポンスの本文値を設定します。

最後の手順は、conf/routesActionのルートを追加することです。

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は、リストの場合よりも複雑です。注意すべき点がいくつかあります。

ボディパーサーは、ケースクラス、明示的な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をサポートするように設計されており、これらのサービスの開発は簡単であるはずです。作業の大部分は、モデルのReadsWritesの記述にあり、これについては次のセクションで詳しく説明します。

次へ:JSON Reads/Writes/Format コンビネータ


このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントのガイドラインをお読みになった後、プルリクエストを送信してください。質問や共有するアドバイスがありますか?コミュニティフォーラムにアクセスして、コミュニティとの会話を始めましょう。