今現在 java json ライブラリを含めないで 少なくとも 6つの scala向け json ライブラリがあります。
すべてのそれらのライブラリは非常に似たASTを持っています。このプロジェクトでは他のscala json ライブラリで使用されるただひとつのASTを提供することを目指しています。
今現在 lift-json 由来の AST を使用するアプローチを取っており、 native パッケージは 事実 lift-json ですが、しかし lift プロジェクトの外部プロジェクトです。
このプロジェクトは lift-json に lift フレームワークより課されてたリリーススケジュールから開放することを試みます。
Lift フレームワークは 多くの依存関係を含んでおり、それらは 新しい scala のバージョンがリリースされた時に たいてい、多くの他 scala プロジェクトへの妨げるになる。
このライブラリの native パッケージは 実際 文字通り 違うパッケージ名の lift-json である。これは このライブラリを使う場合 インポートステートメントが変わることを意味します。
import org.json4s._
import org.json4s.native.JsonMethods._
あとは すべて lift-json と全く同じように動作する。
native パーサーに加えて ASTをパースするのに jackson を使った実装もあります。
jackson モジュールは ほとんどの jackson-module-scala 機能を含んでおり、 lift-json AST を共に使用することができる。
native パーサーに代わって jackson を使うには
import org.json4s._
import org.json4s.jackson.JsonMethods._
jackson 統合のデフォルトの動作では ストリームが完了するとそれがクローズされる動作となることをご留意ください。
もし変更したい場合には:
import com.fasterxml.jackson.databind.SerializationFeature
org.json4s.jackson.JsonMethods.mapper.configure(SerializationFeature.CLOSE_CLOSEABLE, false)
JSON の 分析と フォーマット をするためのユーティリティ
lift-json ライブラリの中心となる概念は、 構文木 として JSONドキュメント構造モデル化した Json AST である。
sealed abstract class JValue
case object JNothing extends JValue // 'zero' for JValue
case object JNull extends JValue
case class JString(s: String) extends JValue
case class JDouble(num: Double) extends JValue
case class JDecimal(num: BigDecimal) extends JValue
case class JInt(num: BigInt) extends JValue
case class JBool(value: Boolean) extends JValue
case class JObject(obj: List[JField]) extends JValue
case class JArray(arr: List[JValue]) extends JValue
type JField = (String, JValue)
すべての機能は上記 AST の用語で実現されている。 機能は AST それ自体の変更、あるいは AST と違うフォーマット間の変換に使用される。一般的な変換は以下の図に要約される。
機能概要:
- 高速な JSON 解析
- LINQ スタイルのクエリ
- 解析された JSON から値を抽出するのに case class を使うことができる。
- 差分 と 結合
- 有効なJSONを生成するDSL
- XPath ライクな式 と JSON を操作する HOFs
- コンパクトに整えられた出力形式
- XML 変換
- シリアル化
- 低レベル pull parser API
以下の方法で json4s の依存関係を追加できます。
注: {latestVersion} を 適切な Json4s のバージョンに置き換えてください。
入手可能なバージョンは こちらから見つけることが出来ます。: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.json4s%22
native を使用する場合 以下の 依存関係をプロジェクトの description に追加してください:
val json4sNative = "org.json4s" %% "json4s-native" % "{latestVersion}"
Jackson を使用する場合 以下の 依存関係をプロジェクトの description に追加してください:
val json4sJackson = "org.json4s" %% "json4s-jackson" % "{latestVersion}"
native を使用する場合、以下の依存関係を pom に追加してください:
<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-native_${scala.version}</artifactId>
<version>{latestVersion}</version>
</dependency>
jackson を使用する場合、以下の依存関係を pom に追加してください:
<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-jackson_${scala.version}</artifactId>
<version>{latestVersion}</version>
</dependency>
Enum, Joda-Time,… のサポート
Scalaz による アプリカティブスタイル解析
Box のサポート
json4s 3.3 は基本的には 3.2.x とソースコード互換であるべきです。 json4s 3.3.0 以降 我々はここに記載のあるバイナリ互換性の問題を繰り返さない為に バイナリ互換性の確認の為に MiMa を使い始めました。
JValueの .toOption
の動作が変更されました。 現在は JNothing
と JNull
の両方が None を返します。
以前の動作の為には JNothing
だけが None になる toSome
を使えます。
すべてのマージされた プルリクエスト: https://github.com/json4s/json4s/pulls?q=is%3Apr+is%3Aclosed+milestone%3A3.3
JField は もはや JValue ではありません。これは さらに 型安全であることを意味し、インスタンスのために直接的にJArrayに加えられたJFieldの 不正なJSONの生成はもはや可能ではない。 この変更のもっとも大きな影響は 2種類の map、transform、find、filter です:
def map(f: JValue => JValue): JValue
def mapField(f: JField => JField): JValue
def transform(f: PartialFunction[JValue, JValue]): JValue
def transformField(f: PartialFunction[JField, JField]): JValue
def find(p: JValue => Boolean): Option[JValue]
def findField(p: JField => Boolean): Option[JField]
//...
JSON の中の field を渡すには *Filed 関数を使います。そして JSONの中の値を渡すときには 名前に'filed'が無い関数を使用します。
Path 式はバージョン2.2以降 変わりました。以前のバージョンでは式の使用で 不必要な複雑な JField が返っていた。もし以下のようなパターンマッチングの path式を使用している場合:
val JField("bar", JInt(x)) = json \ "foo" \ "bar"
次のように変更が必要です:
val JInt(x) = json \ "foo" \ "bar"
有効な json は内部ASTフォーマットに解析されることができる。 native を使用した場合:
scala> import org.json4s._
scala> import org.json4s.native.JsonMethods._
scala> parse(""" { "numbers" : [1, 2, 3, 4] } """)
res0: org.json4s.JsonAST.JValue =
JObject(List((numbers,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))
scala> parse("""{"name":"Toy","price":35.35}""", useBigDecimalForDouble = true)
res1: org.json4s.package.JValue =
JObject(List((name,JString(Toy)), (price,JDecimal(35.35))))
jackson を使用した場合:
scala> import org.json4s._
scala> import org.json4s.jackson.JsonMethods._
scala> parse(""" { "numbers" : [1, 2, 3, 4] } """)
res0: org.json4s.JsonAST.JValue =
JObject(List((numbers,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))
scala> parse("""{"name":"Toy","price":35.35}""", useBigDecimalForDouble = true)
res1: org.json4s.package.JValue =
JObject(List((name,JString(Toy)), (price,JDecimal(35.35))))
json を DoubleMode
あるいは BigDecimalMode
の2つのモードで 生成することが出来る。前者は すべての 10進数の値をJDouble にマップする、後者は JDecimalにマップする。
double モードDSLの使用:
import org.json4s.JsonDSL._
// or
import org.json4s.JsonDSL.WithDouble._
big decimalモードDSLの使用:
import org.json4s.JsonDSL.WithBigDecimal._
- プリミティブ型は JSON プリミティブにマップする。
- いずれの seq は JSON配列を生成する。
scala> val json = List(1, 2, 3)
scala> compact(render(json))
res0: String = [1,2,3]
- Tuple2[String, A] は フィールドを生成する。
scala> val json = ("name" -> "joe")
scala> compact(render(json))
res1: String = {"name":"joe"}
- ~ 演算子は フィールドに組み合わされたオブジェクトを生成する。
scala> val json = ("name" -> "joe") ~ ("age" -> 35)
scala> compact(render(json))
res2: String = {"name":"joe","age":35}
- いずれの値も Option化できる。 フィールドと値はそれが値を持たない時には取り除かれる。
scala> val json = ("name" -> "joe") ~ ("age" -> Some(35))
scala> compact(render(json))
res3: String = {"name":"joe","age":35}
scala> val json = ("name" -> "joe") ~ ("age" -> (None: Option[Int]))
scala> compact(render(json))
res4: String = {"name":"joe"}
- dsl拡張 dslを拡張 あなた自身のクラスと共に スコープ内に引数の適合する暗黙的変換が必要:
type DslConversion = T => JValue
object JsonExample extends App {
import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.jackson.JsonMethods._
case class Winner(id: Long, numbers: List[Int])
case class Lotto(id: Long, winningNumbers: List[Int], winners: List[Winner], drawDate: Option[java.util.Date])
val winners = List(Winner(23, List(2, 45, 34, 23, 3, 5)), Winner(54, List(52, 3, 12, 11, 18, 22)))
val lotto = Lotto(5, List(2, 45, 34, 23, 7, 5, 3), winners, None)
val json =
("lotto" ->
("lotto-id" -> lotto.id) ~
("winning-numbers" -> lotto.winningNumbers) ~
("draw-date" -> lotto.drawDate.map(_.toString)) ~
("winners" ->
lotto.winners.map { w =>
(("winner-id" -> w.id) ~
("numbers" -> w.numbers))}))
println(compact(render(json)))
}
scala> JsonExample
{"lotto":{"lotto-id":5,"winning-numbers":[2,45,34,23,7,5,3],"winners":
[{"winner-id":23,"numbers":[2,45,34,23,3,5]},{"winner-id":54,"numbers":[52,3,12,11,18,22]}]}}
サンプルは 以下の書式を揃えられたJSONを生成します。注意 draw-date フィールドはその値がNoneの為 出力されていない:
scala> pretty(render(JsonExample.json))
{
"lotto":{
"lotto-id":5,
"winning-numbers":[2,45,34,23,7,5,3],
"winners":[{
"winner-id":23,
"numbers":[2,45,34,23,3,5]
},{
"winner-id":54,
"numbers":[52,3,12,11,18,22]
}]
}
}
Please see more examples in MergeExamples.scala and DiffExamples.scala.
二つのJSONはお互いに結合したり、差分を取ったりすることが可能。 詳細はサンプル MergeExamples.scala と DiffExamples.scalaを参照ください。
scala> import org.json4s._
scala> import org.json4s.jackson.JsonMethods._
scala> val lotto1 = parse("""{
"lotto":{
"lotto-id":5,
"winning-numbers":[2,45,34,23,7,5,3],
"winners":[{
"winner-id":23,
"numbers":[2,45,34,23,3,5]
}]
}
}""")
scala> val lotto2 = parse("""{
"lotto":{
"winners":[{
"winner-id":54,
"numbers":[52,3,12,11,18,22]
}]
}
}""")
scala> val mergedLotto = lotto1 merge lotto2
scala> pretty(render(mergedLotto))
res0: String =
{
"lotto":{
"lotto-id":5,
"winning-numbers":[2,45,34,23,7,5,3],
"winners":[{
"winner-id":23,
"numbers":[2,45,34,23,3,5]
},{
"winner-id":54,
"numbers":[52,3,12,11,18,22]
}]
}
}
scala> val Diff(changed, added, deleted) = mergedLotto diff lotto1
changed: org.json4s.JsonAST.JValue = JNothing
added: org.json4s.JsonAST.JValue = JNothing
deleted: org.json4s.JsonAST.JValue = JObject(List((lotto,JObject(List(JField(winners,
JArray(List(JObject(List((winner-id,JInt(54)), (numbers,JArray(
List(JInt(52), JInt(3), JInt(12), JInt(11), JInt(18), JInt(22))))))))))))))
JSON は for-comprehension を用いて抽出することが出来る。 詳細はサンプルJsonQueryExamples.scalaを参照ください。
scala> import org.json4s._
scala> import org.json4s.native.JsonMethods._
scala> val json = parse("""
{ "name": "joe",
"children": [
{
"name": "Mary",
"age": 5
},
{
"name": "Mazy",
"age": 3
}
]
}
""")
scala> for {
JObject(child) <- json
JField("age", JInt(age)) <- child
} yield age
res0: List[BigInt] = List(5, 3)
scala> for {
JObject(child) <- json
JField("name", JString(name)) <- child
JField("age", JInt(age)) <- child
if age > 4
} yield (name, age)
res1: List[(String, BigInt)] = List((Mary,5))
Json AST XPATHライクな機能をつかって問い合わせされることが出来る。 下記のREPLセッションは '\','\\','find','filter','transform','remove','values'関数の使用方法である。
JSON サンプル:
{
"person": {
"name": "Joe",
"age": 35,
"spouse": {
"person": {
"name": "Marilyn",
"age": 33
}
}
}
}
DSL構文への変換:
scala> import org.json4s._
scala> import org.json4s.native.JsonMethods._
あるいは
scala> import org.json4s.jackson.JsonMethods._
scala> import org.json4s.JsonDSL._
scala> val json =
("person" ->
("name" -> "Joe") ~
("age" -> 35) ~
("spouse" ->
("person" ->
("name" -> "Marilyn") ~
("age" -> 33)
)
)
)
scala> json \\ "spouse"
res0: org.json4s.JsonAST.JValue = JObject(List(
(person,JObject(List((name,JString(Marilyn)), (age,JInt(33)))))))
scala> compact(render(res0))
res1: String = {"person":{"name":"Marilyn","age":33}}
scala> compact(render(json \\ "name"))
res2: String = {"name":"Joe","name":"Marilyn"}
scala> compact(render((json removeField { _ == JField("name", JString("Marilyn")) }) \\ "name"))
res3: String = "Joe"
scala> compact(render(json \ "person" \ "name"))
res4: String = "Joe"
scala> compact(render(json \ "person" \ "spouse" \ "person" \ "name"))
res5: String = "Marilyn"
scala> json findField {
case JField("name", _) => true
case _ => false
}
res6: Option[org.json4s.JsonAST.JValue] = Some((name,JString(Joe)))
scala> json filterField {
case JField("name", _) => true
case _ => false
}
res7: List[org.json4s.JsonAST.JField] = List(JField(name,JString(Joe)), JField(name,JString(Marilyn)))
scala> json transformField {
case JField("name", JString(s)) => ("NAME", JString(s.toUpperCase))
}
res8: org.json4s.JsonAST.JValue = JObject(List((person,JObject(List(
(NAME,JString(JOE)), (age,JInt(35)), (spouse,JObject(List(
(person,JObject(List((NAME,JString(MARILYN)), (age,JInt(33)))))))))))))
scala> json.values
res8: scala.collection.immutable.Map[String,Any] = Map(person -> Map(name -> Joe, age -> 35, spouse -> Map(person -> Map(name -> Marilyn, age -> 33))))
インデックス化された path式も動作し、値は型式(データ型を表す式)を使ってアンボックスできます。
scala> val json = parse("""
{ "name": "joe",
"children": [
{
"name": "Mary",
"age": 5
},
{
"name": "Mazy",
"age": 3
}
]
}
""")
scala> (json \ "children")(0)
res0: org.json4s.JsonAST.JValue = JObject(List((name,JString(Mary)), (age,JInt(5))))
scala> (json \ "children")(1) \ "name"
res1: org.json4s.JsonAST.JValue = JString(Mazy)
scala> json \\ classOf[JInt]
res2: List[org.json4s.JsonAST.JInt#Values] = List(5, 3)
scala> json \ "children" \\ classOf[JString]
res3: List[org.json4s.JsonAST.JString#Values] = List(Mary, Mazy)
case class は解析されたJSONから値を抽出し使用することが出来る。存在しない値は scala.Option型に抽出され、文字列は自動的に java.util.Dates 型に変換される。 詳細はサンプルExtractionExampleSpec.scalaを参照ください。
scala> import org.json4s._
scala> import org.json4s.jackson.JsonMethods._
scala> implicit val formats = DefaultFormats // Brings in default date formats etc.
scala> case class Child(name: String, age: Int, birthdate: Option[java.util.Date])
scala> case class Address(street: String, city: String)
scala> case class Person(name: String, address: Address, children: List[Child])
scala> val json = parse("""
{ "name": "joe",
"address": {
"street": "Bulevard",
"city": "Helsinki"
},
"children": [
{
"name": "Mary",
"age": 5,
"birthdate": "2004-09-04T18:06:22Z"
},
{
"name": "Mazy",
"age": 3
}
]
}
""")
scala> json.extract[Person]
res0: Person = Person(joe,Address(Bulevard,Helsinki),List(Child(Mary,5,Some(Sat Sep 04 18:06:22 EEST 2004)), Child(Mazy,3,None)))
scala> val addressJson = json \ "address" // Extract address object
scala> addressJson.extract[Address]
res1: Address = Address(Bulevard,Helsinki)
scala> (json \ "children").extract[List[Child]] // Extract list of objects
res2: List[Child] = List(Child(Mary,5,Some(Sat Sep 04 23:36:22 IST 2004)), Child(Mazy,3,None))
デフォルトでは コンストラクタ パラメータ名は json フィールド名に一致する必要があります。しかし 時々 json フィールド名が Scalaの識別子として許されていない文字を含んでいます。 二つの解決方法があります。(サンプルLottoExample.scala for bigger example)を参照ください)
back ticks(`)を使う.
scala> case class Person(`first-name`: String)
AST後処理に transform 関数を使う。
scala> case class Person(firstname: String)
scala> json transformField {
case ("first-name", x) => ("firstname", x)
}
もし json フィールド名が スネークケース(例: separated_by_underscores)を使用していて ケースクラスが キャメルケース(例:firstLetterLowercaseAndNextWordsCapitalized)を使っている場合
抽出時にcamelizeKeys
メソッドを使って変換することができます。
scala> import org.json4s._
scala> import org.json4s.native.JsonMethods._
scala> implicit val formats = DefaultFormats
scala> val json = parse("""{"first_name":"Mary"}""")
scala> case class Person(firstName: String)
scala> json.camelizeKeys.extract[Person]
res0: Person = Person(Mazy)
キャメルケースのフィールドをスネークケースのキーのjson に変換する方法の詳細はシリアライズの項目を見てください。
抽出関数は case class が補助コンストラクターを持っている場合、もっとも一致するコンストラクターを見つけることを試みます。 例えば JSON {"price":350} を次のcase class に抽出する場合 主コンストラクタの代わりに補助コンストラクタを使用します。
scala> case class Bike(make: String, price: Int) {
def this(price: Int) = this("Trek", price)
}
scala> parse(""" {"price":350} """).extract[Bike]
res0: Bike = Bike(Trek,350)
プリミティブ値はJSONプリミティブ あるいは フィールドに抽出されます。
scala> (json \ "name").extract[String]
res0: String = "joe"
scala> ((json \ "children")(0) \ "birthdate").extract[Date]
res1: java.util.Date = Sat Sep 04 21:06:22 EEST 2004
日付フォーマットは 'DefaultFormats'(あるいは 暗黙的トレイト 'Format')によって上書きされ変更されます。
scala> implicit val formats = new DefaultFormats {
override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
}
JSON オブジェクトは Map[String, _]に抽出することもできる。おのおののフィールドはMapの(キー、値)のペアとなる。
scala> val json = parse("""
{
"name": "joe",
"addresses": {
"address1": {
"street": "Bulevard",
"city": "Helsinki"
},
"address2": {
"street": "Soho",
"city": "London"
}
}
}""")
scala> case class PersonWithAddresses(name: String, addresses: Map[String, Address])
scala> json.extract[PersonWithAddresses]
res0: PersonWithAddresses("joe", Map("address1" -> Address("Bulevard", "Helsinki"),
"address2" -> Address("Soho", "London")))
Case class はシリアライズ、デシリアライズされることができる。
詳細はサンプル SerializationExamples.scala を参照ください。
scala> import org.json4s._
scala> import org.json4s.native.Serialization
scala> import org.json4s.native.Serialization.{read, write}
scala> implicit val formats = Serialization.formats(NoTypeHints)
scala> val ser = write(Child("Mary", 5, None))
scala> read[Child](ser)
res1: Child = Child(Mary,5,None)
もし native の代わりに jackson を使っている場合:
scala> import org.json4s._
scala> import org.json4s.jackson.Serialization
scala> import org.json4s.jackson.Serialization.{read, write}
scala> implicit val formats = Serialization.formats(NoTypeHints)
scala> val ser = write(Child("Mary", 5, None))
scala> read[Child](ser)
res1: Child = Child(Mary,5,None)
もし クラスが キャメルケースフィールド(例:firstLetterLowercaseAndNextWordsCapitalized)を使っていて、
スネークケース(例: separated_by_underscores)の json を生成したくないときは snakizeKeys
メソッドを使えます。
scala> val ser = write(Person("Mary"))
ser: String = {"firstName":"Mary"}
シリアル化 サポート:
scala> compact(render(parse(ser).snakizeKeys))
res0: String = {"first_name":"Mary"}
- 任意の 深い case class グラフ
- BigInt,Symbol を含む すべてのプリミティブ型
- List, Seq, Array, Set と Map (注意, マップのキーは String であることが必要: Map[String, _])
- scala.Option
- java.util.Date
- 多相型List (下を参照)
- 再帰データ型
- クラスフィールドのシリアル化(下を参照)
- サポートされていない型に対する カスタム シリアライザー 関数(下を参照)
多相型(や異なる型を持つ )Listのシリアル化をする際には Type hint が必要です。 シリアル化されたJSONオブジェクトは 'jsonClass' という名前の追加のフィールドを得ます。
scala> trait Animal
scala> case class Dog(name: String) extends Animal
scala> case class Fish(weight: Double) extends Animal
scala> case class Animals(animals: List[Animal])
scala> implicit val formats = Serialization.formats(ShortTypeHints(List(classOf[Dog], classOf[Fish])))
scala> val ser = write(Animals(Dog("pluto") :: Fish(1.2) :: Nil))
ser: String = {"animals":[{"jsonClass":"Dog","name":"pluto"},{"jsonClass":"Fish","weight":1.2}]}
scala> read[Animals](ser)
res0: Animals = Animals(List(Dog(pluto), Fish(1.2)))
ShortTypeHints は 構成されたオブジェクトのすべてのインスタンスのための短いクラス名を出力する。 FullTypeHints は フルクラス名を出力します。 そのほかの方法は TypeHints トレイトを拡張することで実装可能です。
フィールドのシリアル化を有効にするには、FieldSerializerで いくつかの型を追加できる:
implicit val formats = DefaultFormats + FieldSerializer[WildDog]()
WildDog型(と すべてのサブ型)は すべてのフィールド(+ コンストラクタパラメータ)と共にシリアル化される。 FieldSerializerは、フィールドのシリアル化を傍受するために使用できる2つの省略可能なパラメータを受け取ります:
case class FieldSerializer[A: Manifest](
serializer: PartialFunction[(String, Any), Option[(String, Any)]] = Map(),
deserializer: PartialFunction[JField, JField] = Map()
)
それらの 部分関数は フィールドがシリアル化されるか デシリアル化されるその前に呼ばれます。有用な 名前を変える(rename)、フィールドを無視する(ignore) 部分関数が提供されています:
val dogSerializer = FieldSerializer[WildDog](
renameTo("name", "animalname") orElse ignore("owner"),
renameFrom("animalname", "name"))
implicit val formats = DefaultFormats + dogSerializer
われわれは trait に定義された case class のサポートを追加しました。しかし それらには カスタム フォーマットが必要である。「なぜ」 と 「どうやって」 を説明します。
trait の中で定義されたクラスについては そのコンパニオンオブジェクトを取得するのは少し難しいので、デフォルト値を与える必要がある。
われわれは それらをあてはめることはできたが それは コンパイラが そんな case class の コンストラクタに余分なフィールドを生成するという、次の問題を引き起こす。
それらの case class のコンストラクタの はじめのフィールドは $outer
と呼ばれ 定義している trait の型である。 ともかく われわれは そのオブジェクトの インスタンスを取得する必要がある、単純な方法では われわれは すべての class をスキャンし、 trait で定義されている クラスを集めるられるが、しかし 一つ以上ある場合 どれをとればいいか?
私は それらの case class 為の コンパニオン マッピングのリストを含むようにフォーマットを拡張するということを選択しました。
つまり あなたのモジュールに属しているフォーマットを持つことで、そこでマッピングを保持することができます。 そうすることで デフォルト値が動作し $outer
フィールドに必要な多くのものを提供するようになります。
trait SharedModule {
case class SharedObj(name: String, visible: Boolean = false)
}
object PingPongGame extends SharedModule
implicit val formats: Formats =
DefaultFormats.withCompanions(classOf[PingPongGame.SharedObj] -> PingPongGame)
val inst = PingPongGame.SharedObj("jeff", visible = true)
val extr = Extraction.decompose(inst)
extr must_== JObject("name" -> JString("jeff"), "visible" -> JBool(true))
extr.extract[PingPongGame.SharedObj] must_== inst
あらゆる型への カスタム シリアライザ と デシリアライザ 関数を差し込むことが可能。 非 case class である Interval クラスがある場合(かくて、 デフォルトではサポートされていない)われわれは 次の提供されたシリアライザによってシリアル化可能。
scala> class Interval(start: Long, end: Long) {
val startTime = start
val endTime = end
}
scala> class IntervalSerializer extends CustomSerializer[Interval](format => (
{
case JObject(JField("start", JInt(s)) :: JField("end", JInt(e)) :: Nil) =>
new Interval(s.longValue, e.longValue)
},
{
case x: Interval =>
JObject(JField("start", JInt(BigInt(x.startTime))) ::
JField("end", JInt(BigInt(x.endTime))) :: Nil)
}
))
scala> implicit val formats = Serialization.formats(NoTypeHints) + new IntervalSerializer
カスタム シリアライザは 二つの部分関数によって提供される。はじめにそれがJSONからのデータをアンパックできるか値を評価する。 次に 型が適合した場合 望ましいJSONを生成します。
json4s-ext モジュールは抽出とシリアル化の拡張を含んでいます。以下のタイプがサポートされています。
// Lift's box
implicit val formats = org.json4s.DefaultFormats + new org.json4s.native.ext.JsonBoxSerializer
// Scala enums
implicit val formats = org.json4s.DefaultFormats + new org.json4s.ext.EnumSerializer(MyEnum)
// or
implicit val formats = org.json4s.DefaultFormats + new org.json4s.ext.EnumNameSerializer(MyEnum)
// Joda Time
implicit val formats = org.json4s.DefaultFormats ++ org.json4s.ext.JodaTimeSerializers.all
JSON 構造と XMLノードと相互の変換が可能です。
詳細はサンプル XmlExamples.scala を参照ください。
scala> import org.json4s.Xml.{toJson, toXml}
scala> val xml =
<users>
<user>
<id>1</id>
<name>Harry</name>
</user>
<user>
<id>2</id>
<name>David</name>
</user>
</users>
scala> val json = toJson(xml)
scala> pretty(render(json))
res3: String =
{
"users":{
"user":[{
"id":"1",
"name":"Harry"
},{
"id":"2",
"name":"David"
}]
}
}
現在、上のサンプルには2つの問題があります。1つ目 id は Int に変換して欲しかったが String に変換された。これはJString(s) から JInt(s.toInt) へのマッピングをつかい容易に修正が可能です。二つ目はもっと巧妙です。変換機能はJSON配列を使うことを決めます、なぜなら XMLの中に一つ以上の userエレメントがあるからです。それゆえ ただ一つのuserエレメントを持つことが起こる構造的に等しいXMLドキュメントはJSON配列なしの JSONドキュメントを生成します。これは めったに望ましい結果とはならない。これら両方の問題は以下の変換関数によって修正可能である。
scala> json transformField {
case ("id", JString(s)) => ("id", JInt(s.toInt))
case ("user", x: JObject) => ("user", JArray(x :: Nil))
}
他方へもサポートされる。 JSONからXMLへの変換:
scala> toXml(json)
res5: scala.xml.NodeSeq = NodeSeq(<users><user><id>1</id><name>Harry</name></user><user><id>2</id><name>David</name></user></users>)
極限のパフォーマンスが必要される場合のために Pull パーサ APIは提供される。 それは二つの方法で 解析のパフォーマンスを改善します。一つ目は 中間ASTを生成しない。二つ目は 残りのストリームの解析をいつでも停止、スキップできる。注意 この解析スタイルは最適化の場合にのみ推奨される。上で述べられた 関数 API の使用は容易である。
次のサンプルみて どのように JSONからフィールド値を解析するか 検討してください。
scala> val json = """
{
...
"firstName": "John",
"lastName": "Smith",
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": 10021
},
"phoneNumbers": [
{ "type": "home", "number": "212 555-1234" },
{ "type": "fax", "number": "646 555-4567" }
],
...
}"""
scala> val parser = (p: Parser) => {
def parse: BigInt = p.nextToken match {
case FieldStart("postalCode") => p.nextToken match {
case IntVal(code) => code
case _ => p.fail("expected int")
}
case End => p.fail("no field named 'postalCode'")
case _ => parse
}
parse
}
scala> val postalCode = parse(json, parser)
postalCode: BigInt = 10021
Pull パーサは Parser => A
関数です このサンプルの中では 具体的には Parser => BigInt
です。
構造化解析は FieldStart("postalCode")
トークンを見つけるまで再帰的にトークンを読み込む。 その後 次のトークンは IntVal
でなければならない、さもなくば 分析は失敗する。それは 解析された Integer を返し、そして すぐに 解析を中止します。
Q1: 私はJSON オブジェクトがあり、それを case class に抽出したい。
scala> case class Person(name: String, age: Int)
scala> val json = """{"name":"joe","age":15}"""
しかし、抽出に失敗します。:
scala> parse(json).extract[Person]
org.json4s.MappingException: Parsed JSON values do not match with class constructor
A1:
抽出は REPL の中で定義された classに対しては動作しません。 scalac で定義された case class をコンパイルし、それを REPL から インポートしてください。
-
The original idea for DSL syntax was taken from Lift mailing list (by Marius).
-
The idea for AST and rendering was taken from Real World Haskell book.