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

EnglishRussianUkrainian