2016/10/3

json4s: scala json library


scala 世界中,關於處理 json 的 library 很多, 這裡列出了 13 個: Scala standard libray, jawn, sphere-json, argonaut, circe, json4s, spray-json-shapeless, spray-json, lift-json, play-json, rapture,用 google trends 查了一下,json4s查詢的次數還是多一些,json4s 的目標就是提供一個單一的 AST 供其他Scala類庫來使用,以下測試如何使用 json4s。



準備使用 json4s


在 build.sbt 加上 org.json4s 的 library


val json4sVersion = "3.3.0"

libraryDependencies ++= Seq(
  // json4s
  "org.json4s" %% "json4s-native" % json4sVersion,
  "org.json4s" %% "json4s-jackson" % json4sVersion,
  "org.json4s" %% "json4s-ext" % json4sVersion
)

json4s 需要 import 以下的 packages


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

主要提供 parse, render 進行 json 轉換,render 需要輸入 JValue 物件,必須利用 JsonDSL 提供 implicit 轉換,parse 很明確,就是 parsing JSON 字串。


測試 json4s


import java.text.SimpleDateFormat

import org.json4s.JsonDSL._
import org.json4s._
import org.json4s.native.JsonMethods._
import org.scalatest.{FunSpec, Matchers}

class TestJson4s extends FunSpec with Matchers {
  implicit val formats = new DefaultFormats {
    //override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
    override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
  }

  //implicit val formats = Serialization.formats(ShortTypeHints(List()))

  //  implicit val formats = new DefaultFormats {
  //    override val dateFormat: DateFormat = new DateFormat {
  //      override def parse(s: String): Option[Date] = Some(new Date(s.toLong * 1000))
  //      override def format(d: Date): String = (d.getTime/1000).toString
  //    }
  //  }

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

  describe("Testing json4s marshalling, unmarshalling") {
    it("parse json and extract to class") {
      // 將原始的 json 字串,轉換成 json4s JObject 物件
      println()
      println(">將 json 轉換成 JObject")
      val tom = parse( """ { "name" : "tom","age":23,"number":[1,2,3,4] } """)
      println(s"tom:${tom.getClass.getName}")
      println(tom)
      val tstring: String = tom.toString
      //JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))
      tstring should be("JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))")

      // 上面的轉換之後 並不是 Scala 的原生物件
      // 要用 Map[String, _] 轉換
      println()
      println(">把 JObject 轉成 Scala 物件")
      for ((k, v) <- tom.values.asInstanceOf[Map[String, _]]) {
        println(s"${k} -> ${v}:${v.getClass}")
      }
      //      name -> tom:class java.lang.String
      //      age -> 23:class scala.math.BigInt
      //      number -> List(1, 2, 3, 4):class scala.collection.immutable.$colon$colon

      // 可以用  \  取得某個欄位的值
      println()
      println(">可以用  \\  取得某個欄位的值")
      println(s"name -> ${(tom \ "name").values}")
      println(s"age -> ${(tom \ "age").values}")
      println(s"number -> ${(tom \ "number").values}")

      //      name -> tom
      //      age -> 23
      //      number -> List(1, 2, 3, 4)

      val name = (tom \ "name").values
      name should be("tom")

      // 可直接將 tom 轉換成 Student

      val student: Student = tom.extract[Student]
      println()
      println(">可直接將 tom 轉換成 Student")
      println(student)
      // Student(tom,23)

      student.name should be("tom")

      ///// 列印 json

      println()
      println("> 列印 json 時要呼叫 compact")
      println(compact(render(Map("name" -> "tom", "age" -> "23"))))
      //结果:{"name":"tom","age":"23"}

      println()
      println("> 只支援 Tuple2[String, A] 這種格式的 tuple")
      val tuple = ("name", "tom")
      println(compact(render(tuple)))
      //结果:{"name":"tom"}


      println()
      println("> 可以用 ~ 將兩個 tuple 連接在一起")
      val tuple2 = ("name", "tom") ~ ("age", 23)
      println(tuple2)
      println(compact(render(tuple2)))
      //结果:
      //JObject(List((name,JString(tom)), (age,JInt(23))))
      //{"name":"tom","age":23}


      println()
      println("> 當 value 為 None 時,就不會序列化")
      val tuple3 = ("name", "tom") ~("age", None: Option[Int])
      println(tuple3)
      //结果:
      //JObject(List((name,JString(tom)), (age,JNothing)))
      //{"name":"tom"}


      println()
      println("> 處理 date 必須要加上 implicit val formats, 用 pretty 可以讓列印結果容易閱讀")
      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, Option(new java.util.Date()))
      val customDateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
      val json =
        ("lotto" ->
          ("lotto-id" -> lotto.id) ~
            ("winning-numbers" -> lotto.winningNumbers) ~
            //("draw-date" -> lotto.drawDate.map(_.toString)) ~
            ("draw-date" -> lotto.drawDate.map( customDateFormatter.format(_) )) ~
            ("winners" ->
              lotto.winners.map { w =>
                (("winner-id" -> w.id) ~
                  ("numbers" -> w.numbers))
              }))

      println(compact(render(json)))
      // {"lotto":{"lotto-id":5,"winning-numbers":[2,45,34,23,7,5,3],"draw-date":"2016-05-19 12:03:36","winners":[{"winner-id":23,"numbers":[2,45,34,23,3,5]},{"winner-id":54,"numbers":[52,3,12,11,18,22]}]}}

      println(pretty(render(json)))
      //
      //      {
      //        "lotto":{
      //          "lotto-id":5,
      //          "winning-numbers":[2,45,34,23,7,5,3],
      //          "draw-date":"2016-05-19 12:03:36",
      //          "winners":[{
      //          "winner-id":23,
      //          "numbers":[2,45,34,23,3,5]
      //        },{
      //          "winner-id":54,
      //          "numbers":[52,3,12,11,18,22]
      //        }]
      //        }
      //      }


      //
      println()
      println("> 用 merge 合併兩個 json")
      val t1 = parse(
        """
    {"name":"tom",
       "age":23,
       "class":["xiaoerban"]
    }
        """)
      val t2 = parse(
        """
    {"name":"tom",
       "age":23,
       "class":["xiaosanban"]
    }
        """)
      val t3 = t1 merge t2
      println(pretty(render(t3)))
      //      {
      //        "name":"tom",
      //        "age":23,
      //        "class":["xiaoerban","xiaosanban"]
      //      }

      println()
      println("> 用 Diff 檢查兩個 JObject 的差異")
      val Diff(changed, added, deleted) = t3 diff t1
      println("changed", changed)
      println("added", added)
      println("deleted", deleted)
      //      (changed,JNothing)
      //      (added,JNothing)
      //      (deleted,JObject(List((class,JArray(List(JString(xiaosanban)))))))


      println()
      println("> 可以將 xml 轉換成 json")
      import org.json4s.Xml.toJson
      val xml =
        <users>
          <user>
            <id>1</id>
            <name>Harry</name>
          </user>
          <user>
            <id>2</id>
            <name>David</name>
          </user>
        </users>

      val json2 = toJson(xml)
      println(pretty(render(json2)))
    }

  }
}

以下為執行的結果


>將 json 轉換成 JObject
tom:org.json4s.JsonAST$JObject
JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))

>把 JObject 轉成 Scala 物件
name -> tom:class java.lang.String
age -> 23:class scala.math.BigInt
number -> List(1, 2, 3, 4):class scala.collection.immutable.$colon$colon

>可以用  \  取得某個欄位的值
name -> tom
age -> 23
number -> List(1, 2, 3, 4)

>可直接將 tom 轉換成 Student
Student(tom,23)

> 列印 json 時要呼叫 compact
{"name":"tom","age":"23"}

> 只支援 Tuple2[String, A] 這種格式的 tuple
{"name":"tom"}

> 可以用 ~ 將兩個 tuple 連接在一起
JObject(List((name,JString(tom)), (age,JInt(23))))
{"name":"tom","age":23}

> 當 value 為 None 時,就不會序列化
JObject(List((name,JString(tom)), (age,JNothing)))

> 處理 date 必須要加上 implicit val formats, 用 pretty 可以讓列印結果容易閱讀
{"lotto":{"lotto-id":5,"winning-numbers":[2,45,34,23,7,5,3],"draw-date":"2016-05-19 13:57:54","winners":[{"winner-id":23,"numbers":[2,45,34,23,3,5]},{"winner-id":54,"numbers":[52,3,12,11,18,22]}]}}
{
  "lotto":{
    "lotto-id":5,
    "winning-numbers":[2,45,34,23,7,5,3],
    "draw-date":"2016-05-19 13:57:54",
    "winners":[{
      "winner-id":23,
      "numbers":[2,45,34,23,3,5]
    },{
      "winner-id":54,
      "numbers":[52,3,12,11,18,22]
    }]
  }
}

> 用 merge 合併兩個 json
{
  "name":"tom",
  "age":23,
  "class":["xiaoerban","xiaosanban"]
}

> 用 Diff 檢查兩個 JObject 的差異
(changed,JNothing)
(added,JNothing)
(deleted,JObject(List((class,JArray(List(JString(xiaosanban)))))))

> 可以將 xml 轉換成 json
{
  "users":{
    "user":[{
      "id":"1",
      "name":"Harry"
    },{
      "id":"2",
      "name":"David"
    }]
  }
}
[info] TestJson4s:
[info] Testing json4s marshalling, unmarshalling
[info] - parse json and extract to class
[info] Run completed in 371 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Reference


學習和使用Scala的Json解析類庫-JSON4S