Работа с JSON в Scala при помощи библиотеки json4s

23 марта 2015

Все мы любим JSON. Это простой формат, хорошо читаемый, удобный при отладке, стандарт де-факто во всяких там REST-ах, и не только. Более того, JSON может быть еще и довольно компактным, например, если передавать список с именами полей один раз, а за ним — списки значений. Или если просто сжать его при помощи gzip. В мире Scala есть немало библиотек для работы с JSON, но наиболее мощной и производительной, видимо, является json4s.

Для подключения json4s к проекту прописываем в build.sbt:

"org.json4s" %% "json4s-jackson" % "3.2.11"

Простой пример декодирования JSON:

import org.json4s._
import org.json4s.jackson.JsonMethods._

object Json4sTests extends App {
  val t = parse("""{"postId": 123123123123123, "text":"ololo"}""")
  println(t)
}

Результат:

JObject(List((postId,JInt(123123123123123)), (text,JString(ololo))))

То есть, получили AST.

С его же помощью можно собрать и сериализовать JSON объект:

val obj = JObject(List(
  "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")

Результат:

compact:
{"postId":123123123123123,"text":"ololo"}

pretty:
{
  "postId":123123123123123,
  "text":"ololo"
}

Строительство AST при помощи JObject’ов и JString’ов довольно многословно, давайте это исправим:

import org.json4s._
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)))
}

Результат:

{"type":"post","info":{"postId":12345,"tags":["ololo","trololo"]}}

Что еще многословно — это парсить AST используя только паттерн матчинг. Поэтому json4s предлагает XPath-подобные комбинаторы, вроде тех, что мы использовали в свое время при парсинге XML:

val json = parse(
  """|{"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")

Результат:

postIdsObj = List(ololo, trololo)
firstPostId = 1

А что, если у нас есть набор каких-то case class’ов и мы хотели бы сериализовать их в JSON? Писать сериализацию и десериализацию вручную? Конечно же нет:

import org.json4s._
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")
}

Вывод программы:

json:
{
  "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-поля и поля со значениями по умолчанию, а также удалять поля, и это не сломает обратную совместимость. То есть, программа, в которой были сделаны такие изменения, сможет десериализовать объект, сериализованный программой до внесения изменений. Проверьте сами!

Наконец, рассмотрим последний пример. Допустим, мы хотим сериализовать данные вроде таких:

object Operation extends Enumeration {
  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)
  )
}

В этом случае мы получим ошибку:

Exception in thread "main" org.json4s.package$MappingException: Do not
know how to serialize key of type class scala.Enumeration$Val. Consider
implementing a CustomKeySerializer.

… потому что ключами в JSON-объектах могут быть только строки. Нам нужно как-то отображать OperationType на строки и обратно. Сказано — сделано:

val OperationSerializer = new CustomKeySerializer[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")

Вывод программы:

json:
{
  "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 и при помощи какой библиотеки работаете с ним?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.