Play

play-json便利だよね?って話

More than 3 years have passed since last update.

これは Play framework 2.x Scala Advent Calendar 2013 の 14日目です。

先日は@seratchさん(2日ぶり5回目)のPlay ドキュメントを Skinny で書くと - Manipulating Results, Session and Flash scopes, Content negotiationでした
てか13日のやつ12日深夜の段階で埋まってなかったんですけど、すかさず埋められるセラさんすげぇ…

play-json的な何か

playでjsonを扱うことは多々あると思います

ありますよね?

play-jsonに関しては正直本家ドキュメント以上の資料無いと思います
なので既にplay-jsonゴリゴリ使っているぜ!という方には物足りないかも…

これからplayを触ろうとしている、社内のお隣チーム向けでしょうかw
こんなことできるよ的な

あと、play-jsonを使いたくなければ、
play-json4sというプラグインとかもあります。

逆にplay-jsonをplay以外で使いたければ2.2系からは使えるようになっています
(2.1系のときはplay-json-aloneとかありました)

// こんなかんじで使えます

resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

libraryDependencies ++=Seq(
  "com.typesafe.play" %% "play-json" % "2.2.1"
)

transformが便利というお話

ここのお話です
最初の英語頑張る気力がない俺のような人向けの素敵な翻訳版はこちら
versionが2.1.5と2.2.xと差がありますが、play-jsonに関しては大きな差はないはず…
(jacksonのversionが変わったとかはありますが)

ちなみに上のドキュメントの最後の例とか、個人的には「おお…すげぇ…」以上の感想持ってないんですけどね…

普段自分が何にtransformを使っているかというと

  • ユニットテストで使うJsonを作成するとき
  • スコープが限られた範囲でのJsonの変換
    • 今社内のログフォーマットをjson lines 形式にしているので、それをちょっと加工するときとかに使ってます

jsonサンプルを一つ作って、transformで色々と変換してテストに使ってます

こんなかんじに使っています(Specsとしての動作確認してませんw)

import org.specs2._
import play.api.http.MimeTypes
import play.api.libs.json._
import play.api.mvc._
import play.api.test.Helpers._

class HogeSpec extends Specification {

  def is = s2"""

test1 $e1

    """

  val targetController = new TargetController extends Controller // なんかのController

  val json = Json.parse("""
  {
    "user": {
      "name" : "hoge 太郎",
      "age" : 25,
      "sex" : "male",
      "email" : "hoge@example.com",
      "job" : "programmer"
    }
  }
  """)

  def e1 = {
    val res = targetController.hoge()(FakeRequest(Helpers.POST, "/hoge").withJsonBody(json).withHeaders(Helpers.CONTENT_TYPE -> MimeTypes.JSON))
    (status(res) === OK).updateMessage("レスポンスコードが期待したものではない")
  }
}

当然年齢を変えてみたり、性別を変えてみたり、jobをneetにしたり
もちろん間違った値を入れてみたり…とかしたいですよね?

そこでtransformの出番です

import Reads._

val ageTransform = { age:Int => __.json.update( (__ \ 'user \ 'age).json.put(JsNumber(age))) }

val sexTransform = (__ \ 'user \ 'sex).json.update( of[JsString].map{
    case JsString("male") => JsString("female")
    case JsString("female") => JsString("male")
    })

val jobChange = __.json.update( (__ \ 'user \ 'job).json.put(JsString("neet")))

val illigalSex = __.json.update( (__ \ 'user \ 'sex).json.put(JsBoolean(true)))

わりと色々な書き方ができてしまうので…
その辺は時と場合とおこのみで…

  def hogeResponse(js:JsValue) = {
    targetController.hoge()(FakeRequest(Helpers.POST, "/hoge").withJsonBody(js).withHeaders(Helpers.CONTENT_TYPE -> MimeTypes.JSON))
  }

  def e2 = {
    json.transform(ageTransform(30)).fold(
        invalid = e => anError,
        valid = js => (status(hogeResponse(js)) === OK).toResult
    )
  }

  def e3 = {
    json.transform(sexTransform).fold(
        invalid = e => anError,
        valid = js => (status(hogeResponse(js)) === OK).toResult
    )
  }

  def e4 = {
    json.transform(jobChange).fold(
        invalid = e => anError,
        valid = js => (status(hogeResponse(js)) === OK).toResult
    )
  }

  def e5 = {
    json.transform(illigalSex).fold(
        invalid = e => anError,
        valid = js => (status(hogeResponse(js)) === BAD_REQUEST).toResult
    )
  }

jobChangeしてニートってなんだかわかりませんが…こんなかんじに使えます

合成する

ともすると、次は念のため30歳でneetのケースも確かめたくなるのが人情でしょう?

大丈夫、andThenで合成できます

  def e6 = {
    val neet30 = ageTransform(30) andThen jobChange
    json.transform(neet30).fold(
      invalid = e => anError,
      valid = js => (status(hogeResponse(js)) === BAD_REQUEST).toResult
    )
  }

30歳neetの時はBAD_REQUESTを返す… そいういう仕様にしたいということがわかります…

まとめ

要は、変換ルールを書いて組み合わせてtransformに食わせればOK! ってことです

とは言うものの、上記ぐらい簡単な例だと、年齢とかを引数に取ってJsValueを返すようなメソッド作ればいいかもしれません。
が、Jsonそのものが複雑になってくると引数が増えてきたり…と結構辛くなる場合が多いと思います

また、上記例ではinvalidを単に捨てていますが、返ってくるJsErrorを見ればだいたい何を間違ったのかもわかります
(これはtransformというよりはJsResult便利って話ですけど)

まあ、社内でもあんまりtransform使っているコード見てない(俺の観測範囲が狭い?)ので、そんなに好かれていないのかなぁ…
でも個人的には好きです(※)

transform今まで使ったことねーわーという人、これを気に触ってみてはいかがでしょう

※ 個人的に好きなモノ、気に入ったものはあまり他人が好きになってくれない事多いからこれももしかして…

最近社内で聞かれたことまとめ

serializeするときにvalueをnullにしたい

case class Hoge(a:Int, b:Option[String])

val writes = (
  (__ \ 'a).write[Int] ~
  (__ \ 'b).writeNullable[String]
  )(unlift(Hoge.unapply))

val h = Hoge(1, None)

Json.toJson(h)(writes)

とやると

{
  "a" : 1
}

となります

これを

{
  "a" : 1,
  "b" : null
}

にしたいというお話

val writes = (
    (__ \ 'a).write[Int] ~
    (__ \ 'b).write[Option[String]]
    )(unlift(Hoge.unapply))

とか

nullableは実装を見るとNoneの場合 Json.obj() と完全に何もない空のJsObjectを返します
一方OptionWritesを見ると、Noneの場合は JsNull が返ります

という差でキーができたりできなかったり

他にもoptionWithNullとかあるっぽいですね 使ったこと無いけど

intかStringで来るvalueをどうにかしたい

{
  "id" : 1234,
  "name" : "hoge"
}

{
  "id" : "a123",
  "name" : "fuga"
}

古いシステムと新しいシステム混ざっているとありそうですね

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

case class Hoge(id:String, name:String)

val reads = (
    (__ \ 'id).read(of[String] | of[Int].map(_.toString)) ~
    (__ \ 'name).read[String]
    )( Hoge.apply _)

val json = Json.parse("""
{
  "id" : 1234,
  "name" : "hoge"
}
""")

val json2 = json.transform(__.json.update((__ \ 'id).json.put(JsString("a123")))).get

assert(json.validate(reads) == JsSuccess(Hoge("1234", "hoge")))
assert(json2.validate(reads) == JsSuccess(Hoge("a123", "hoge")))

とか

書き方色々とありますが、個人的にはこんなかんじでやってます

まとめ

上のやつも含めて答えはだいたいドキュメントにあります
そう、結構play-json系便利〜ってこれまでも言いたかったんですけど、だいたいドキュメントにあるんで言いづらかったんです…

それとReads.scala Writes.scala あたりを
ボケーッっと眺めると既に用意してくれているものとかに気がつけますのでオススメ

自分は jodaTime 読み書き用のものが既に準備されているのにそれで気が付きましたw

明日は @hiro0107 さんです