Play Framework では json を扱うためのライブラリ play-json が提供されています。 扱いやすいライブラリだと思うのですがいつも細かい API を忘れてしまうので整理しておきたいと思いました。

ここで使用しているコードは こちら のリポジトリに保管しています。

  1. Json を表現する型
  2. 文字列との単純な相互変換
  3. Json に対する操作
  4. Reads, Writes, Format 型
  5. 複雑な Reads, Writes の作成

1. Json を表現する型

play-json では json を JsValue 型で表現しており、そのデータ型は Haskell 風に書けば以下のようになります。

data JsValue = JsObject (Map String JsValue)
             | JsArray [JsValue]
             | JsNumber BigDecimal
             | JsString String
             | JsBoolean Boolean
             | JsNull

2. 文字列との単純な相互変換

文字列と JsValue との変換は それぞれ Json.parse, Json.stringify を使用できます。

val rawString =
  """
    |{
    |  "name": "Alice",
    |  "age": 20,
    |  "favorites": ["tennis", "swimming"],
    |  "family": [
    |    {
    |      "name": "Bob",
    |      "relation": "father"
    |    },
    |    {
    |      "name": "Catharin",
    |      "relation": "mother"
    |    }
    |  ]
    |}
  """.stripMargin

// String to JsValue
// `Json.parse` throws exception for invalid input
val json: JsValue = Json.parse(rawString)
println(s"parsed json: $json")

// JsValue to String
// `Json.prettyPrint` is also available to print pretty json
println(s"reversed to string: ${Json.stringify(json)}")

3. Json に対する操作

Json 上のデータを簡単に取得するには \, \\ を使用します。

// Traverse json by `\`
// JsLookupResult is like 
// data JsLookupResult = JsDefined JsValue
//                     | JsUndefined String
val lookupName: JsLookupResult = json \ "name"
println(lookupName.get) // "Alice"

val lookupUnknown = json \ "xxx"
lookupUnknown match {
  case JsDefined(value) =>
    println(value)
  case err @ JsUndefined() =>
    println(err) // JsUndefined('xxx'List([{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}]) is undefined on object: {"name":"Alice","age":20,"favorites":["tennis","swimming"],"family":[{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}]})
}

// `\` can be repeated
println(json \ "favorites" \ 1) // JsDefined("swimming")
println(json \ "xxx" \ "yyy") // JsUndefined('xxx' is undefined on object: {"name":"Alice","age":20,"favorites":["tennis","swimming"],"family":[{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}]})

// Retrieve values by `\\`
val selectFamily: Seq[JsValue] = json \\ "family"
println(selectFamily) // List([{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}])
println(json \\ "xxx") // List()

4. Reads, Writes, Format 型クラス

実際のプログラミングでは与えられた json をそのまま json として扱うより適切なオブジェクトへと変換して扱い、また必要ならば json へと変換したいという場合が多いと思います。

そうした場合 play-json で用意されている Reads, Writes 型クラスを使用します。

Reads 型クラス

あるクラスを Reads にするためには reads(json: JsValue): JsResult[A] を実装する必要があります (他のものはデフォルト実装があり、ここでは省略している)。

trait Reads[A] { self =>
  /**
   * Convert the JsValue into a A
   */
  def reads(json: JsValue): JsResult[A]

  def map[B](f: A => B): Reads[B] = ...
  def flatMap[B](f: A => Reads[B]): Reads[B] = ...
  def filter(f: A => Boolean): Reads[A] = ...
  def filter(error: JsonValidationError)(f: A => Boolean): Reads[A] = ...
  def filterNot(f: A => Boolean): Reads[A] = ...
  def filterNot(error: JsonValidationError)(f: A => Boolean): Reads[A] = ...
  def collect[B](error: JsonValidationError)(f: PartialFunction[A, B]): Reads[B] = ...
  def orElse(v: Reads[A]): Reads[A] = ...
  def compose[B <: JsValue](rb: Reads[B]): Reads[A] = ...
  def andThen[B](rb: Reads[B])(implicit witness: A <:< JsValue): Reads[B] = ...
}

Reads のコンパニオンオブジェクトには以下のような apply が実装されているので、これを利用して自分で Reads を定義することができます。

object Reads extends ConstraintReads with PathReads with DefaultReads with GeneratedReads {
  ...
  def apply[A](f: JsValue => JsResult[A]): Reads[A] =
    new Reads[A] { def reads(json: JsValue) = f(json) }
  ...
}

例えば単純ですが “name” だけを持つ json から Name クラスのオブジェクトへと変換する処理は以下のように書けます (ただ json を扱う上で必要になる Reads(e.g. String, Int) は既に play-json で定義されているので、実際は 複雑な Reads, Writes の作成 で見るような方法の方がスッキリします)。

Name クラス

import play.api.libs.json._

class Name(val value: String)

object Name {
  // JsValue => JsResult
  implicit val reads = Reads[Name] { json =>
    (json \ "name") match {
      case JsDefined(name)     => JsSuccess(new Name(name.as[String]))
      case err @ JsUndefined() => JsError(err.toString)
    }
  }
}

変換処理

val nameJson = Json.obj("name" -> "Alice")
println(nameJson.as[Name].value) // Alice
println(nameJson.validate[Name]) // JsSuccess(com.tiqwab.example.json.Name@318ba8c8,)
println(Json.fromJson[Name](nameJson)) // JsSuccess(com.tiqwab.example.json.Name@318ba8c8,)

val illegalNameJson = Json.obj("xxx" -> "Alice")
println(illegalNameJson.validate[Name]) // JsError(List((,List(JsonValidationError(List(JsUndefined('name' is undefined on object: {"xxx":"Alice"})),WrappedArray())))))

as は失敗時に例外を投げるので、validate を使用するほうが安全です。validateJsSuccess または JsError 値コンストラクタを持つ JsResult 型を返します。

またケースクラスの場合は Json.reads によりマクロで Reads の実装が提供できます。

case class Age(age: Int)

object Age {
  // Parse json like {"age" -> 20}
  implicit val reads = Json.reads[Age]
}

Writes 型クラス

Writes 型クラスを実装したクラスは json へと変換することができるようになります。 Reads と同様にこちらは writes(o: A): JsValue を実装すれば ok で、他はデフォルト実装が存在します。

オブジェクトから json への変換には OWrites という型クラスもあるようなのですが、これが別途用意されている理由はわかっていないです。

trait Writes[-A] {
  /**
   * Convert the object into a JsValue
   */
  def writes(o: A): JsValue

  def transform(transformer: JsValue => JsValue): Writes[A] = ...
  def transform(transformer: Writes[JsValue]): Writes[A] = ...
}

trait OWrites[-A] extends Writes[A] {
  def writes(o: A): JsObject

  def transform(transformer: JsObject => JsObject): OWrites[A] = ...
  def transform(transformer: OWrites[JsObject]): OWrites[A] = ...
}

上で使用した Name, AgeWrites を実装してみます。

import play.api.libs.json._

class Name(val value: String)

object Name {
  ...
  implicit val writes = Writes[Name] { name =>
    Json.obj("name" -> name.value)
  }
}
import play.api.libs.json.{Json, Reads}

case class Age(age: Int)

object Age {
  ...
  implicit val writes = Json.writes[Age] // Add writes by macro
}

Json.toJson で implicit に定義した Writes を使用して json への変換が行なえます。

val name = new Name("Alice")
println(Json.toJson(name)) // {"name":"Alice"} 
val age = Age(20)
println(Json.toJson(age)) // {"age":20}

Format 型クラス

上で Reads, Writes 型クラスを別々に定義しましたが、これをペアにした 型クラスとして Format というものがあります。

trait Format[A] extends Writes[A] with Reads[A]
trait OFormat[A] extends OWrites[A] with Reads[A] with Format[A]

object Format extends PathFormat with ConstraintFormat with DefaultFormat {

  val constraints: ConstraintFormat = this
  val path: PathFormat = this

  implicit val invariantFunctorFormat: InvariantFunctor[Format] =
    new InvariantFunctor[Format] {
      def inmap[A, B](fa: Format[A], f1: A => B, f2: B => A) =
        Format(fa.map(f1), Writes(b => fa.writes(f2(b))))
    }

  def apply[A](fjs: Reads[A], tjs: Writes[A]): Format[A] = new Format[A] {
    def reads(json: JsValue) = fjs.reads(json)
    def writes(o: A) = tjs.writes(o)
  }
}

例えばケースクラスへの定義時に Json.Format を使用すれば自動的に Reads, Writes に対応したクラスを作成することができます。

case class Age(age: Int)

object Age {
  implicit val format = Json.format[Age]
}

5. 複雑な Reads, Writes の作成

JSON Reads/Writes/Formats Combinators を使用します。

Reads

例えば簡単な例として以下の Person クラスを考えます。

case class Person(name: String, age: Int) {}

以下のような json から Person を作成してみます。

{
    "name": "Alice",
    "age": 20
}
object ReadsCombinator extends App {
  // To show types
  // val namePath: JsPath = JsPath \ 'name
  // val nameReads: Reads[String] = namePath.read[String]
  // val builderReads: FunctionalBuilder[Reads]#CanBuild2[String, Int] =
  //   (JsPath \ "name").read[String] ~
  //     (JsPath \ "age").read[Int]

  implicit val personReads: Reads[Person] = (
    (JsPath \ "name").read[String] ~
      (JsPath \ "age").read[Int]
  )(Person.apply _)

  val json = Json.obj("name" -> "Alice", "age" -> 20)
  json.validate[Person] match {
    case s: JsSuccess[Person] => println(s.get) // Person(Alice, 20)
    case e: JsError           => println(e)
  }
}

やっていることは何となくわかりますが、 personReads: Reads[Person] の定義周りの動作が捉えにくく感じます。 この部分を理解しようと思うとじっくり腰を据えないと厳しそうなのですが、ここではざっくりと何が起きているのかだけでも理解したいところです。

そこでこの部分を

  • FunctionalBuilder[Reads]#CanBuild2[String, Int] を生成する
  • Reads[Person] を生成する

の 2 つの処理に分解して整理してみます。

play-json のコードを追った感じ、FunctionalBuilder[Reads]#CanBuild2[String, Int] は以下のような implicit 定義が使用されて生成されているようです。

  • Applicative[M] であれば FunctionalCanBuild[M] に implicit conversion できる
package play.api.libs.functional

object FunctionalCanBuild {
  implicit def functionalCanBuildApplicative[M[_]](implicit app: Applicative[M]): FunctionalCanBuild[M] = new FunctionalCanBuild[M] {
    def apply[A, B](a: M[A], b: M[B]): M[A ~ B] = app.apply(app.map[A, B => A ~ B](a, a => ((b: B) => new ~(a, b))), b)
  }
}
  • FunctionalCanBuild[M] ならば FunctionalBuilderOps[M[_], A] に implicit conversion できる
  • FunctionalBuilderOps[M[_], A] ならば ~ から FunctionalBuilder[M]#CanBuild2[A, B] が生成できる
package play.api.libs.functional

class FunctionalBuilderOps[M[_], A](ma: M[A])(implicit fcb: FunctionalCanBuild[M]) {

  def ~[B](mb: M[B]): FunctionalBuilder[M]#CanBuild2[A, B] = {
    val b = new FunctionalBuilder(fcb)
    new b.CanBuild2[A, B](ma, mb)
  }

  def and[B](mb: M[B]): FunctionalBuilder[M]#CanBuild2[A, B] = this.~(mb)
}
  • Reads は Applicative
package play.api.libs.json

object Reads extends ConstraintReads with PathReads with DefaultReads with GeneratedReads {
  ...

  import play.api.libs.functional._

  implicit def applicative(
    implicit applicativeJsResult: Applicative[JsResult]
  ): Applicative[Reads] = new Applicative[Reads] {
    def pure[A](a: A): Reads[A] = Reads.pure(a)
    def map[A, B](m: Reads[A], f: A => B): Reads[B] = m.map(f)
    def apply[A, B](mf: Reads[A => B], ma: Reads[A]): Reads[B] = new Reads[B] {
      def reads(js: JsValue) = applicativeJsResult(mf.reads(js), ma.reads(js))
    }

  ...
  }

FunctionalBuilder[Reads]#CanBuild2[String, Int] からの Reads[Person] 生成には以下の apply が使用されているようです。

package play.api.libs.functional

class FunctionalBuilder[M[_]](canBuild: FunctionalCanBuild[M]) {

  class CanBuild2[A1, A2](m1: M[A1], m2: M[A2]) {

    def ~[A3](m3: M[A3]) = new CanBuild3[A1, A2, A3](canBuild(m1, m2), m3)

    def apply[B](f: (A1, A2) => B)(implicit fu: Functor[M]): M[B] =
      fu.fmap[A1 ~ A2, B](canBuild(m1, m2), { case a1 ~ a2 => f(a1, a2) })

    ...
  }
  ...
}

Writes

Reads で力尽きたのですが、 Writes も考え方はまるっと同じはずです…

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

object WritesCombinator extends App {
  // def play.api.libs.functional.syntax.unlift[A, B](f: A => Option[B]): (A) => B
  implicit val personReads: Writes[Person] = (
    (JsPath \ "name").write[String] ~
      (JsPath \ "age").write[Int]
  )(unlift(Person.unapply))

  val json = Json.obj("name" -> "Alice", "age" -> 20)
  val person = Person("Alice", 20)
  println(Json.toJson(person)) // {"name":"Alice","age":20}
}

最後にこれらを使用して冒頭の json に対応する Person クラスに ReadsWrites を与えます。

Person, FamilyMember モデルの作成

import play.api.libs.json.{JsPath}
import play.api.libs.functional.syntax._

case class Person(
    name: String,
    age: Int,
    favorites: Seq[String],
    family: Seq[FamilyMember]
)

object Person {
  // Define by macro
  // implicit val format = Json.format[Person]

  // Define manually
  implicit val format = (
    (JsPath \ "name").format[String] ~
      (JsPath \ "age").format[Int] ~
      (JsPath \ "favorites").format[Seq[String]] ~
      (JsPath \ "family").format[Seq[FamilyMember]]
  )(Person.apply _, unlift(Person.unapply _))
}
import play.api.libs.json.Json

case class FamilyMember(name: String, relation: String)

object FamilyMember {
  implicit val format = Json.format[FamilyMember]
}

Person オブジェクトと json の相互変換

import play.api.libs.json._

object ComplicatedReadsCombinator extends App {
  val rawString =
    """
      |{
      |  "name": "Alice",
      |  "age": 20,
      |  "favorites": ["tennis", "swimming"],
      |  "family": [
      |    {
      |      "name": "Bob",
      |      "relation": "father"
      |    },
      |    {
      |      "name": "Catharin",
      |      "relation": "mother"
      |    }
      |  ]
      |}
    """.stripMargin

  val json = Json.parse(rawString)
  val person = json.validate[com.tiqwab.example.json.model.Person].get
  println(person) // Person(Alice,20,List(tennis, swimming),List(FamilyMember(Bob,father), FamilyMember(Catharin,mother)))
  println(Json.stringify(Json.toJson(person))) // {"name":"Alice","age":20,"favorites":["tennis","swimming"],"family":[{"name":"Bob","relation":"father"},{"name":"Catharin","relation":"mother"}]}
}

参考