lift-json

Scala pro práci s XML a JSON daty nabízí hned několik užitečných nástrojů. Pro XML je to Scala.XML ze standardní knihovny a pak výborná Anti-xml (o obou těchto knihovnách jsem psal už dříve). V případě JSONu pak můžeme použít lift-json, který býval nedílnou součástí web-frameworku Lift, ale nedávno byl vyčleněn do osamostatněn projektu json4s.
lift-json
toho nabízí hodně: rychlý JSON parser, AST reprezentaci JSON dat, jejich
manipulaci, transformaci, merge a diff, dotazy v duchu LINQu, selektory ve stylu XPath výrazů, DSL pro konstrukci
JSON dat, konverzi JSONu z a do XML, serializaci objektů do JSONu a extrakci z JSONu do case class
.
Ze všech těchto možností je nejzajímavější právě ta poslední. Umožnuje nám skoro až zázračně zkonvertovat JSON dokument do odpovídajícího stromu objektů.
Jako příklad si vezměte hypotetické zpracování data z Twitteru:
// nejdřív si deklarujeme hierarchii case class, která reflektuje strukturu JSONu case class Tweet(id: Long, created_at: String, text: String, user: User) case class User(id: Long, name: String, screen_name: String, url: String, description: String) // importujeme všechno potřebné import net.liftweb.json._ // tohle je potřeba mj. pro formátování datumů implicit val formats = DefaultFormats // načteme data val url = "http://api.twitter.com/1/statuses/user_timeline.json?screen_name=kaja47" val jsonString = io.Source.fromURL(url).mkString // extrahujeme JSON do tříd val tweets: List[Tweet] = parse(jsonString).extract[List[Tweet]] // data jsou staticky typovaná a můžeme s nimi pracovat jako s každým jiným objektem val usersCounts: Map[String, Int] = tweets.map(_.user.screen_name).groupBy(identity).mapValues(_.size)
Jako mávnutím kouzelného proutku nám lift-json namapoval JSON dokument na
hierarchii objektů. Z JSON pole se stane List
, JSON objekt se namapuje na
case class, všechny jeho vlastnosti na vlastnosti třídy s odpovídajícím jménem
a typem. Kdyby data neodpovídala struktuře objektů (chybějící vlastnosti
objektu nebo odlišné typy), metoda extract
vyhodí výjimku.
Pokud jsou v JSON objektu některé položky volitelné, musíme je v naší case
class
reprezentovat typem Option
. Když tomu tak nebude, extraktor v případě
chybějících nepovinných položek zahlásí, že data neodpovídají požadovanému
formátu.
Dále je poměrně obvyklé, že objekty se kterými pracuje naše aplikace mají jiná jména atributů (camel case vs. underscore), jiné typy nebo jiný tvar. V tom případě musíme vytvořit funkci, která umí transformovat jednu reprezentaci na druhou. Většinou jde o triviální mapování.
Nic ale není ideální a i tady je jeden zádrhel, který musíme brát na vědomí.
Case class
mají jedno zásadní omezení: nemohou mít víc než 22 atributů (není žádný
zvláštní důvod proč právě 22, tvůrci jazyka zkrátka někde museli nakreslit čáru do
písku a jako maximum zvolili 22). To má nepříjemný důsledek: když JSON
API nějaké služby vrací obrovské objekty s mnoha atributy,
nemůžeme je extrahovat do jedné case class
.
Jako příklad může posloužit API 4chanu, které vrací objekty s 29 atributy.
Když narazíme na takový případ, je nutné použít nějaké ty obezličky. Můžeme data z JSON objektu dostat nějak jinak nebo objekt rozdělit na několik „pohledů“, každý extrahovat zvlášť a pak je spojit do požadovaného objektu (většinou to nepřidá žádnou práci navíc, protože by nějaké mapování stejně probíhalo, kód je jenom o něco málo složitější a ošklivější).
// jeden pohled, který má 14 atributů case class ApiPost( no: Long, resto: Long, sticky: Option[Int], closed: Option[Int], time: Long, name: Option[String], trip: Option[String], id: Option[String], capcode: Option[String], country: Option[String], countryName: Option[String], email: Option[String], sub: Option[String], com: Option[String] ) // další pohled s 11 atributy case class ApiImage( tim: Option[Long], filename: Option[String], ext: Option[String], fsize: Option[Int], md5: Option[String], w: Option[Int], h: Option[Int], tn_w: Option[Int], tn_h: Option[Int], filedeleted: Option[Int], spoiler: Option[Int] ) import net.liftweb.json._ implicit val formats = DefaultFormats // výsledný kompozitní objekt case class Post { ... } // naše funkce, která spojí naše dva "pohledy" do finálního objektu def merge(post: ApiPost, image: ApiImage): Post = ??? val JArray(posts) = parse(jsonString) \ "posts" val res: Seq[Post] = posts map { postJson => merge(postJson.extract[ApiPost], postJson.extract[ApiImage]) }
lift-json
se dá také trochu paradoxně použít pro konverzi XML dokumentů na
case class
. Stačí jenom XML převést na JSONové AST a to pak extrahovat. Musíme
si dát pozor na určité chování konvertoru, ale je to možné.
import net.liftweb.json._ import net.liftweb.json.Xml.{toJson, toXml} val xml: NodeSeq = getSomeData() val posts = toJson(xml).extract[List[Post]]