ScalaでJSONライブラリというと、以下あたりがいいのかな?
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 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)
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)
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
のように修正すれば、まったく同じように動作します。