Работа с JSON в Scala при помощи библиотеки json4s
23 марта 2015
Все мы любим JSON. Это простой формат, хорошо читаемый, удобный при отладке, стандарт де-факто во всяких там REST-ах, и не только. Более того, JSON может быть еще и довольно компактным, например, если передавать список с именами полей один раз, а за ним — списки значений. Или если просто сжать его при помощи gzip. В мире Scala есть немало библиотек для работы с JSON, но наиболее мощной и производительной, видимо, является json4s.
Для подключения json4s к проекту прописываем в build.sbt:
Простой пример декодирования JSON:
import org.json4s.jackson.JsonMethods._
object Json4sTests extends App {
val t = parse("""{"postId": 123123123123123, "text":"ololo"}""")
println(t)
}
Результат:
То есть, получили AST.
С его же помощью можно собрать и сериализовать JSON объект:
"postId" -> JInt(123123123123123L),
"text" -> JString("ololo")
))
val doc = render(obj)
val compactJson = compact(doc)
val prettyJson = pretty(doc)
println(s"compact:\n$compactJson\n\npretty:\n$prettyJson")
Результат:
{"postId":123123123123123,"text":"ololo"}
pretty:
{
"postId":123123123123123,
"text":"ololo"
}
Строительство AST при помощи JObject’ов и JString’ов довольно многословно, давайте это исправим:
import org.json4s.jackson.JsonMethods._
import org.json4s.JsonDSL._
object Json4sTests extends App {
val obj = ("type" -> "post") ~
("info" ->
("postId" -> 12345L) ~
("tags" -> Seq("ololo", "trololo"))
)
println(compact(render(obj)))
}
Результат:
Что еще многословно — это парсить AST используя только паттерн матчинг. Поэтому json4s предлагает XPath-подобные комбинаторы, вроде тех, что мы использовали в свое время при парсинге XML:
"""|{"posts":[{"id":1,"text":"ololo"},
|{"id":2,"text":"trololo"}]}""".stripMargin
)
val postTexts: List[String] = {
json \ "posts" \\ "text" \ classOf[JString]
}
println(s"postTexts = $postTexts")
val JInt(firstPostId) = (json \ "posts")(0) \ "id"
println(s"firstPostId = $firstPostId")
Результат:
firstPostId = 1
А что, если у нас есть набор каких-то case class’ов и мы хотели бы сериализовать их в JSON? Писать сериализацию и десериализацию вручную? Конечно же нет:
import org.json4s.jackson.JsonMethods._
import org.json4s.jackson.Serialization
object Json4sTests extends App {
sealed trait Status
case object StatusOk extends Status
case object StatusBanned extends Status
case class User(name: (String, String, String), friends: Seq[User],
status: Option[Status])
// implicit val formats = Serialization.formats(NoTypeHints)
implicit val formats = {
Serialization.formats(FullTypeHints(List(classOf[Status])))
}
val john = {
val jane = User(("Jane", "J", "Doe"), Nil, Some(StatusBanned))
User(("John", "J", "Doe"), Seq(jane), None)
}
val json = pretty(render(Extraction.decompose(john)))
println(s"json:\n$json")
val decodedUser = parse(json).extract[User]
println(s"decoded user: $decodedUser")
}
Вывод программы:
{
"name" : {
"_1" : "John",
"_2" : "J",
"_3" : "Doe"
},
"friends" : [ {
"name" : {
"_1" : "Jane",
"_2" : "J",
"_3" : "Doe"
},
"friends" : [ ],
"status" : {
"jsonClass" : "Json4sTests$StatusBanned$"
}
} ]
}
decoded user: User((John,J,Doe),List(User((Jane,J,Doe),List(),
Some(StatusBanned))),None)
Как видите, все на месте, никакие данные не потерялись. Что интересно, кортежи из двух элементов кодируются как {"Jane":"Doe"}
:) Если же воспользоваться альтернативным значением неявного аргумента formats, который с NoTypeHints, то программа тоже будет работать, причем в полученном JSON’е не будет страшной строчки "Json4sTests$StatusBanned$"
. Но значение поля status будет потеряно — при десериализации мы будем всегда получать None. Собственно, это логично, должен же status как-то кодироваться.
Что еще интересно, мы можем добавлять в сериализуемые классы Option-поля и поля со значениями по умолчанию, а также удалять поля, и это не сломает обратную совместимость. То есть, программа, в которой были сделаны такие изменения, сможет десериализовать объект, сериализованный программой до внесения изменений. Проверьте сами!
Наконец, рассмотрим последний пример. Допустим, мы хотим сериализовать данные вроде таких:
val READ, WRITE = Value
}
type OperationType = Operation.Value
case class Stat(min: Double, max: Double, sum: Double, count: Long)
val stats: Map[OperationType, Stat] = {
Map(
Operation.READ -> Stat(1.0, 2.0, 15.0, 7L),
Operation.WRITE -> Stat(0.5, 3.0, 13.0, 8L)
)
}
В этом случае мы получим ошибку:
know how to serialize key of type class scala.Enumeration$Val. Consider
implementing a CustomKeySerializer.
… потому что ключами в JSON-объектах могут быть только строки. Нам нужно как-то отображать OperationType на строки и обратно. Сказано — сделано:
format => (
{ case s: String => Operation.withName(s) },
{ case k: OperationType => k.toString }
))
implicit val serializationFormats = {
Serialization.formats(NoTypeHints) + OperationSerializer
}
val json = pretty(render(Extraction.decompose(stats)))
println(s"json:\n$json")
val decodedStat = parse(json).extract[Map[OperationType, Stat]]
println(s"decodedStat:\n$decodedStat")
Вывод программы:
{
"READ" : {
"min" : 1.0,
"max" : 2.0,
"sum" : 15.0,
"count" : 7
},
"WRITE" : {
"min" : 0.5,
"max" : 3.0,
"sum" : 13.0,
"count" : 8
}
}
decodedStat:
Map(READ -> Stat(1.0,2.0,15.0,7), WRITE -> Stat(0.5,3.0,13.0,8))
Все в полном соответствии с нашими ожиданиями!
Больше примеров использования json4s вы можете найти на официальном сайте библиотеки, а также в репозитории на GitHub. Особое внимание уделите каталогу tests. Например, в нем можно найти интересный файлик XmlExamples.scala. Да, json4s также поддерживает и XML!
А как вы относитесь к JSON и при помощи какой библиотеки работаете с ним?
Метки: Scala, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.