← На главную

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

Все мы любим 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 и при помощи какой библиотеки работаете с ним?