k47.cz  — každý den dokud se vám to nezačne líbit
foto Praha výběr povídky kultura
twitter FB RSS

lift-json

22. 11. 2012 — k47 (CC by-nc) (♪)

Scala pro práci s XML a JSON daty nabízí hned ně­ko­lik uži­teč­ných ná­strojů. Pro XML je to Scala.XML ze stan­dardní knihovny a pak vý­borná Anti-xml (o obou těchto knihov­nách jsem psal už dříve). V pří­padě JSONu pak můžeme použít lift-json, který býval ne­díl­nou sou­částí web-fra­meworku Lift, ale ne­dávno byl vy­čle­něn do osa­mo­stat­něn pro­jektu json4s.

lift-json toho nabízí hodně: rychlý JSON parser, AST re­pre­zen­taci JSON dat, jejich ma­ni­pu­laci, trans­for­maci, merge a diff, dotazy v duchu LINQu, se­lek­tory ve stylu XPath výrazů, DSL pro kon­strukci JSON dat, kon­verzi JSONu z a do XML, se­ri­a­li­zaci ob­jektů do JSONu a ex­trakci z JSONu do case class.

Ze všech těchto mož­ností je nej­za­jí­ma­vější právě ta po­slední. Umož­nuje nám skoro až zá­zračně zkon­ver­to­vat JSON do­ku­ment do od­po­ví­da­jí­cího stromu ob­jektů.

Jako pří­klad si vez­měte hy­po­te­tické zpra­co­vání data z Twit­teru:

// 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áv­nu­tím kou­zel­ného proutku nám lift-json na­ma­po­val JSON do­ku­ment na hi­e­rar­chii ob­jektů. Z JSON pole se stane List, JSON objekt se na­ma­puje na case class, všechny jeho vlast­nosti na vlast­nosti třídy s od­po­ví­da­jí­cím jménem a typem. Kdyby data ne­od­po­ví­dala struk­tuře ob­jektů (chy­bě­jící vlast­nosti ob­jektu nebo od­lišné typy), metoda extract vyhodí vý­jimku.

Pokud jsou v JSON ob­jektu ně­které po­ložky vo­li­telné, musíme je v naší case class re­pre­zen­to­vat typem Option. Když tomu tak nebude, ex­trak­tor v pří­padě chy­bě­jí­cích ne­po­vin­ných po­lo­žek za­hlásí, že data ne­od­po­ví­dají po­ža­do­va­nému for­mátu.

Dále je po­měrně ob­vyklé, že ob­jekty se kte­rými pra­cuje naše apli­kace mají jiná jména atri­butů (camel case vs. un­der­score), jiné typy nebo jiný tvar. V tom pří­padě musíme vy­tvo­řit funkci, která umí trans­for­mo­vat jednu re­pre­zen­taci na druhou. Vět­ši­nou jde o tri­vi­ální ma­po­vá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í ome­zení: ne­mo­hou mít víc než 22 atri­butů (není žádný zvláštní důvod proč právě 22, tvůrci jazyka zkrátka někde museli na­kres­lit čáru do písku a jako ma­xi­mum zvo­lili 22). To má ne­pří­jemný dů­sle­dek: když JSON API nějaké služby vrací ob­rov­ské ob­jekty s mnoha atri­buty, ne­mů­žeme je ex­tra­ho­vat do jedné case class.

Jako pří­klad může po­slou­žit API 4chanu, které vrací ob­jekty s 29 atri­buty.

Když na­ra­zíme na takový případ, je nutné použít nějaké ty obe­z­ličky. Můžeme data z JSON ob­jektu dostat nějak jinak nebo objekt roz­dě­lit na ně­ko­lik „po­hledů“, každý ex­tra­ho­vat zvlášť a pak je spojit do po­ža­do­va­ného ob­jektu (vět­ši­nou to ne­přidá žádnou práci navíc, pro­tože by nějaké ma­po­vání stejně pro­bí­halo, kód je jenom o něco málo slo­ži­tější a oš­k­li­vě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 pa­ra­doxně použít pro kon­verzi XML do­ku­mentů na case class. Stačí jenom XML pře­vést na JSO­Nové AST a to pak ex­tra­ho­vat. Musíme si dát pozor na určité cho­vání kon­ver­toru, 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]]

píše k47 & hosté, ascii@k47.cz