読者です 読者をやめる 読者になる 読者になる

GeekFactory

int128.hatenablog.com

Play 2.1 ScalaでJSONを受け取ってJSONを返す

play scala

Play for ScalaJSONを読み書きする方法について書きます。

ドキュメントではReads/Writes/Format combinatorsを定義する方法が書かれています。

val customReads: Reads[(String, Float, List[String])] = 
  (JsPath \ "key1").read[String](email keepAnd minLength(5)) and 
  (JsPath \ "key2").read[Float](min(45)) and
  (JsPath \ "key3").read[List[String]] 
  tupled
http://www.playframework.com/documentation/2.1.1/ScalaJsonCombinators

この方法では入れ子構造のJSONをパースするやり方が分かりませんでした。 \\ で複数マッチできるようですが、うまくいかず。

別のページで、Scala 2.10から導入されたマクロを使う方法が書かれています。

case class Person(name: String, age: Int, lovesChocolate: Boolean)

implicit val personReads = Json.reads[Person]
http://www.playframework.com/documentation/2.1.1/ScalaJsonInception

この方法を使ってみましょう。

おおまかな流れ

今回は下記のようなJSONを受け取るサービスを考えます。

{
  "title": "ほげほげのあんけーと",
  "description": "なんちゃら勉強会について",
  "questions": [
    { "description": "ほげほげセッションはおもしろかったですか?" },
    { "description": "ふがふがセッションはおもしろかったですか?" }
  ]
}

このJSONに対応するクラスは下記のようになります。

case class EnqueteDto(title: String,
                      description: Option[String],
                      questions: List[QuestionDto])

case class QuestionDto(description: String)

case classに対応するFormatを定義すればJSONを読み書きできるようになります。

import play.api.mvc._
import play.api.libs.json._

object Application extends Controller {
  implicit val questionReads = Json.format[QuestionDto]
  implicit val enqueteReads = Json.format[EnqueteDto]

  def createEnquete = Action(parse.json) { request =>
    request.body.validate[EnqueteDto].map { dto =>
      // オッケーの場合はIDを返す
      Ok(Json.obj("id" -> EnqueteService.create(dto)))
    }.recoverTotal { e =>
      // バリデーションエラーを返す
      BadRequest(Json.obj("error" -> JsError.toFlatJson(e)))
    }
  }
}

パターン

ここでは、HTTPやJSON変換のコードはcontrollersに置き、ドメインのコードはmodels/servicesに置くものとします。すると、先ほどのcase classはサービスのインタフェースを表すのでservicesに属するのが自然かと思います。

package services

import models._
import play.api.db.slick.Config.driver.simple._
import play.api.db.slick.DB
import play.api.Play.current

object EnqueteService {
  case class QuestionDto(description: String)
  case class EnqueteDto(title: String, description: Option[String], questions: List[QuestionDto])

  def create(dto: EnqueteDto): String = DB.withTransaction { implicit session =>
    // Enquetes.insert(...)
    // Questions.insert(...)
    "TODO"
  }
}

本稿で説明した内容は下記のようにパターン化できます。

  • HTTP router
  • controllers (JSONをもらってJSONを返す)
  • services (DTOをもらってDTOを返す)
  • models