Posted on Tue 18 September 2012

Beautiful JSON parsing in Scala

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:

1
2
3
4
5
6
7
8
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 ([cached]scala.util.parsing.json.JSON), but it is terrible slow and the output is still not very nice.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 [cached]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 [cached]scala.Dynamic which allows us to handle arbitrary method calls. That means we will be able to use JSON like this:

1
2
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:

1
2
3
4
5
6
7
8
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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

© Julian Schrittwieser. Built using Pelican. Theme by Giulio Fidente on github. .