Beautiful JSON parsing in Scala
Tue 18 September 2012
You probably all know JSON - it's becoming the universal data exchange format, slowly but steadily replacing XML. In JavaScript, JSON is a proper first class citizen:
person = { "name": "Joe Doe", "age": 45, "kids": ["Frank", "Marta", "Joan"] }; person.age; // 45 person.kids[1]; // "Marta"
Sadly, it's not as easy in other languages. Scala does have a JSON parser in the standard library (scala.util.parsing.json.JSON), but it is terrible slow and the output is still not very nice.
val person_json = """{ "name": "Joe Doe", "age": 45, "kids": ["Frank", "Marta", "Joan"] }""" val person = scala.util.parsing.json.JSON.parseFull(person_json) // returns "Joe Doe" person match { case Some(m: Map[String, Any]) => m("name") match { case s: String => s } }
Luckily, it's easy to create a better and faster parser. I used the json-smart library to do the actual parsing (it's really fast!) and wrote a wrapper in Scala to make the results nicer to use.
A very important ingredient here is scala.Dynamic
which allows us to handle arbitrary method calls. That means we will be able to use JSON like this:
person.name.toString // "Joe Doe" person.kids(1).toString // "Marta"
Strictly speaking, the toString
is not even necessary, as it's easy to define implicit conversions, but it makes it clearer what kind of result we expect.
Everything is built on two dynamic methods:
def selectDynamic(name: String): ScalaJSON = apply(name) // used for object.field def applyDynamic(name: String)(arg: Any) = { // used for object.method(parm) arg match { case s: String => apply(name)(s) case n: Int => apply(name)(n) case u: Unit => apply(name) // sometimes called when field is accessed } }
The rest is really just a wrapper for the data structures returned by json-smart
, but it allows you to write really concise code:
package EasyJSON import JSON._ object Test extends App { print("Fetching recent data ...") val json = io.Source.fromURL("http://liquid8002.untergrund.net/infinigag/").mkString println(" done\n") val tree = parseJSON(json) println("next page: %d\n".format(tree.attributes.next.toInt)) println("=== Top %d images from 9gag ===".format(tree.images.length)) tree.images.foreach { case image => println("%s: %s".format(image.title, image.image.thumb)) } }
Check out the full implementation here (it also allows you to generate JSON):
package EasyJSON import net.minidev.json.JSONValue import net.minidev.json.JSONArray import net.minidev.json.JSONObject import scala.collection.JavaConversions._ object JSON { def parseJSON(s: String) = new ScalaJSON(JSONValue.parse(s)) def makeJSON(a: Any): String = a match { case m: Map[String, Any] => m.map { case (name, content) => "\"" + name + "\":" + makeJSON(content) }.mkString("{", ",", "}") case l: List[Any] => l.map(makeJSON).mkString("[", ",", "]") case l: java.util.List[Any] => l.map(makeJSON).mkString("[", ",", "]") case s: String => "\"" + s + "\"" case i: Int => i.toString } implicit def ScalaJSONToString(s: ScalaJSON) = s.toString implicit def ScalaJSONToInt(s: ScalaJSON) = s.toInt implicit def ScalaJSONToDouble(s: ScalaJSON) = s.toDouble } case class JSONException extends Exception class ScalaJSONIterator(i: java.util.Iterator[java.lang.Object]) extends Iterator[ScalaJSON] { def hasNext = i.hasNext() def next() = new ScalaJSON(i.next()) } class ScalaJSON(o: java.lang.Object) extends Seq[ScalaJSON] with Dynamic { override def toString: String = o.toString def toInt: Int = o match { case i: Integer => i case _ => throw new JSONException } def toDouble: Double = o match { case d: java.lang.Double => d case f: java.lang.Float => f.toDouble case _ => throw new JSONException } def apply(key: String): ScalaJSON = o match { case m: JSONObject => new ScalaJSON(m.get(key)) case _ => throw new JSONException } def apply(idx: Int): ScalaJSON = o match { case a: JSONArray => new ScalaJSON(a.get(idx)) case _ => throw new JSONException } def length: Int = o match { case a: JSONArray => a.size() case m: JSONObject => m.size() case _ => throw new JSONException } def iterator: Iterator[ScalaJSON] = o match { case a: JSONArray => new ScalaJSONIterator(a.iterator()) case _ => throw new JSONException } def selectDynamic(name: String): ScalaJSON = apply(name) def applyDynamic(name: String)(arg: Any) = { arg match { case s: String => apply(name)(s) case n: Int => apply(name)(n) case u: Unit => apply(name) } } }
Tags: programming, web
italics | surround text with *asterisks* |
bold | surround text with **two asterisks** |
hyperlink | [hyperlink](https://example.com)or just a bare URL |
code | surround text with `backticks` |
surround text with ~~two tilde characters~~ | |
quote | prefix with > |
Loading comments...