CLOVER🍀

That was when it all began.

json4sで遊ぶ

Scalaで、JSONを使ってみたくなりまして。

ScalaJSONライブラリというと、以下あたりがいいのかな?

json4s
https://github.com/json4s/json4s
http://json4s.org/

argonaut
http://argonaut.io/

jackson-module-scala
https://github.com/FasterXML/jackson-module-scala

今回は、比較的メジャーであると思われる?json4sを使ってみることにしました。Jacksonのサポートもありますし。

JSONの扱うのに、nativeかJacksonかを選ぶみたいですが、とりあえずnativeからいってみます。

準備

まずは依存関係の定義。
build.sbt

name := "json4s-native-example"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.4"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked")

incOptions := incOptions.value.withNameHashing(true)

libraryDependencies ++= Seq(
  "org.json4s" %% "json4s-native" % "3.2.8",
  "org.scalatest" %% "scalatest" % "2.1.3" % "test"
)

Scala 2.11.0が先日Maven Centralにアップされましたが、json4sはまだ対応が終わっていないので、今回はScala 2.10.4を使用します。

テストコードは、ScalaTestで。

雛形は、こんな感じ。
src/test/scala/org/littlewings/json4snative/Json4sNativeSpec.scala

package org.littlewings.json4snative

import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods
import org.json4s.native.Serialization

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class Json4sNativeSpec extends FunSpec {
  // ここに、テストコードを書く!
}

JSONをパースして、クエリを使う

JSONを表現した文字列をパースする時は、JsonMethods#parseを使用します。

Parsing JSON
https://github.com/json4s/json4s#parsing-json

      val json = """|{
                    |  "numbers": [1, 2, 3, 4]
                    |}""".stripMargin

      val jsonValue = JsonMethods.parse(json)

結果はjson4sのASTとなるようですが、valuesを使用すれば標準の型で返してくれるみたいです。

      jsonValue should be (a [JObject])

      jsonValue.values should be (Map("numbers" -> List(1, 2, 3, 4)))
      jsonValue.children should be (
        List(
          JArray(
            List(JInt(1), JInt(2), JInt(3), JInt(4)))))

ASTに対しては、クエリを投げることも可能。

      val four =
        for {
          JObject(child) <- jsonValue
          JField("numbers", numbers) <- child
          JInt(n) <- numbers if n == 4
        } yield n

      four should be (List(4))

Querying JSON("LINQ" style)
https://github.com/json4s/json4s#linq-style

JSON文字列を作成する

DSLを使って、JSONを組み立てるようです。

DSL rules
https://github.com/json4s/json4s#dsl-rules

例1。

      val source =
        ("numbers" -> List(1, 2, 3, 4))

      JsonMethods.compact(JsonMethods.render(source)) should be ("""{"numbers":[1,2,3,4]}""")

例2。

      val source =
        ("name" -> "some organization") ~
        ("persons" -> List(
          ("name" -> "Taro") ~ ("age" -> 22),
          ("name" -> "Hanako") ~ ("age" -> 18),
          ("name" -> "Saburo") ~ ("age" -> 25)
        ))

      JsonMethods.compact(JsonMethods.render(source)) should be {
        """{"name":"some organization","persons":[{"name":"Taro","age":22},{"name":"Hanako","age":18},{"name":"Saburo","age":25}]}"""
      }

名前と値のペアは、Scalaのタプルとして表現して、兄弟要素は「~」でつなぐようです。

Case Classを使ってみる

今度は、Case Classを使ってJSONとの相互変換をやってみます。

定義したCase Classは、こんな感じで。
src/main/scala/org/littlewings/json4snative/CaseClasses.scala

package org.littlewings.json4snative

case class Organization(name: String, persons: List[Person])

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

シリアライズScalaJSON)。

      val organization =
        Organization(
          "some organization",
          List(Person("Taro", 22), Person("Hanako", 18), Person("Saburo", 25))
        )

      implicit val formats = Serialization.formats(NoTypeHints)
      Serialization.write(organization) should be {
        """{"name":"some organization","persons":[{"name":"Taro","age":22},{"name":"Hanako","age":18},{"name":"Saburo","age":25}]}"""

Implicit Valをここで定義しているのが、ポイントみたいです。

      implicit val formats = Serialization.formats(NoTypeHints)

続いて、デシリアライズJSONScala)。

      val json =
        """|{"name":"some organization",
           | "persons":
           |  [{"name":"Taro","age":22},
           |   {"name":"Hanako","age":18},
           |   {"name":"Saburo","age":25}]}""".stripMargin

      implicit val formats = DefaultFormats
      val parsedJson = JsonMethods.parse(json)
      val organization = parsedJson.extract[Organization]

      organization should be (Organization("some organization",
                                           List(Person("Taro", 22),
                                                Person("Hanako", 18),
                                                Person("Saburo", 25))))

ここでは、以下のImplicit Valを定義することがポイントのようで。

      implicit val formats = DefaultFormats

Case Classでなかったら?

個人的には、ScalaをBetter Java的な使い方をしている時が多いので、Case Classじゃなくて普通のClassやvarを使ったりしたら、どうなるかということで。
src/main/scala/org/littlewings/json4snative/SimpleClasses.scala

package org.littlewings.json4snative

import java.util.Objects

class NoCaseOrganization(val name: String, val persons: List[NoCasePerson]) {
  override def equals(target: Any): Boolean = target match {
    case other: NoCaseOrganization => name == other.name && persons == other.persons
    case _ => false
  }

  override def hashCode: Int =
    Objects.hash(name, persons)
}

class NoCasePerson(val name: String, val age: Int) {
  override def equals(target: Any): Boolean = target match {
    case other: NoCasePerson => name == other.name && age == other.age
    case _ => false
  }

  override def hashCode: Int =
    Objects.hash(name, age: Integer)
}

class VarPerson(var name: String, var age: Int) {
  def this() = this(null, 0)
}

一見、普通に動くように見えます。

    it("Serialization #Val") {
      val organization =
        new NoCaseOrganization(
          "some organization",
          List(new NoCasePerson("Taro", 22), new NoCasePerson("Hanako", 18), new NoCasePerson("Saburo", 25))
        )

      implicit val formats = Serialization.formats(NoTypeHints)
      Serialization.write(organization) should be {
        """{"name":"some organization","persons":[{"name":"Taro","age":22},{"name":"Hanako","age":18},{"name":"Saburo","age":25}]}"""
      }
    }

    it("Deserialization #Val") {
      val json =
        """|{"name":"some organization",
           | "persons":
           |  [{"name":"Taro","age":22},
           |   {"name":"Hanako","age":18},
           |   {"name":"Saburo","age":25}]}""".stripMargin

      implicit val formats = DefaultFormats
      val parsedJson = JsonMethods.parse(json)
      val organization = parsedJson.extract[NoCaseOrganization]

      organization should be (new NoCaseOrganization("some organization",
                                                     List(new NoCasePerson("Taro", 22),
                                                          new NoCasePerson("Hanako", 18),
                                                          new NoCasePerson("Saburo", 25))))
    }

フィールドをvarにした場合。

    it("Serialization #Var") {
      val person = new VarPerson
      person.name = "Taro"
      person.age = 22

      implicit val formats = Serialization.formats(NoTypeHints)
      Serialization.write(person) should be ("""{"name":"Taro","age":22}""")
    }

    it("Deserialization #Var") {
      val json =
        """|{"name":"Taro",
           | "age": 22}""".stripMargin

      implicit val formats = DefaultFormats
      val parsedJson = JsonMethods.parse(json)
      val person = parsedJson.extract[VarPerson]

      person.name should be ("Taro")
      person.age should be (22)
    }
  }

なんですけど、どうもコンストラクタでフィールド定義をしないとマッピングしてくれなさそう?自分でルールを書いたら、なんとかなるのかもしれないですが…。

というわけで、Javaのライブラリと合わせて使う(普通のJavaBeansを期待する)場合は、こんな定義になっちゃうのかな…と。

class VarPerson(var name: String, var age: Int) {
  def this() = this(null, 0)
}

Jacksonは?

上記コード例だと、ほとんど変更するところがありません。

build.sbtで

  "org.json4s" %% "json4s-native" % "3.2.8",

を使っていたところを

  "org.json4s" %% "json4s-jackson" % "3.2.8",

のように修正します。

あとは、json4sを使ったimport文を

import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.jackson.JsonMethods
import org.json4s.jackson.Serialization

のように修正すれば、まったく同じように動作します。