k47.cz
mastodon twitter RSS
bandcamp explorer

lift-json

22. 11. 2012 (před 10 lety) — k47 (CC by-nc)

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]]

píše k47, ascii@k47.cz