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