Sunday, August 30, 2009

JSON Serialization for Scala Objects

SJSON : JSON Serialization for Scala Objects has just been published on the Github. It uses the awesome dispatch-json of Nathan Hamblen as the base JSON processor.

Here's the idea ..

I have a Scala object as ..

val addr = Address("Market Street", "San Francisco", "956871")

which I would like to store as JSON and retrieve as plain old Scala object. Here's the simple assertion that I would like to have as invariant ..

addr should equal(
  serializer.in[Address](serializer.out(addr)))


There are situations, particularly when writing generic libraries, when I don't know what class to serialize into. I can do that as well ..

serializer.in[AnyRef](serializer.out(addr))

or just as ..

serializer.in(serializer.out(addr))

What you get back from is a JsValue, an abstraction of the JSON object model. You can use extractors to get back individual attributes ..

val a = serializer.in[AnyRef](serializer.out(addr))

// use extractors
val c = 'city ? str
val c(_city) = a
_city should equal("San Francisco")

val s = 'street ? str
val s(_street) = a
_street should equal("Market Street")

val z = 'zip ? str
val z(_zip) = a
_zip should equal("956871")


Serialization of Embedded Objects

Suppose you have the following Scala classes .. Here Contact has an embedded Address Map ..

@BeanInfo
case class Contact(name: String, 
                   @JSONTypeHint(classOf[Address])
                   addresses: Map[String, Address]) {
  
  private [json] def this() = this(null, null)
  
  override def toString = "name = " + name + " addresses = " + 
    addresses.map(=> a._1 + ":" + a._2.toString).mkString(",")
}

@BeanInfo
case class Address(street: String, city: String, zip: String) {
  private [json] def this() = this(null, null, null)
  
  override def toString = "address = " + street + "/" + city + "/" + zip
}


With SJSON, I can do the following ..

val a1 = Address("Market Street", "San Francisco", "956871")
val a2 = Address("Monroe Street", "Denver", "80231")
val a3 = Address("North Street", "Atlanta", "987671")

val c = Contact("Bob", Map("residence" -> a1, "office" -> a2, "club" -> a3))
val co = serializer.out(c)

// with class specified
c should equal(serializer.in[Contact](co))

  // no class specified
val a = serializer.in[AnyRef](co)

// extract name
val n = 'name ? str
val n(_name) = a
"Bob" should equal(_name)

// extract addresses
val addrs = 'addresses ? obj
val addrs(_addresses) = a

// extract residence from addresses
val res = 'residence ? obj
val res(_raddr) = _addresses

// make an Address bean out of _raddr
val address = JsBean.fromJSON(_raddr, Some(classOf[Address]))
a1 should equal(address)

object r { def >[T](f: JsF[T]) = f(a.asInstanceOf[JsValue]) }

// still better: chain 'em up
"Market Street" should equal(
  (>{ ('addresses ? obj) andThen ('residence ? obj) andThen ('street ? str) }))


Feel free to fork, contribute and enjoy!

5 comments:

Anonymous said...

Looks very impressive, and demonstrates the power of scala very well !

Anonymous said...

Looks neat. Your choice of fonts, however, makes my eyes bleed.

Dhananjay Nene said...

Cute!

Just a couple of nitpicks :

.in and .out are confusing and their directionality is really in the eye of the reader (I would've imagined them to be behaving exactly the other way around)

The usage of JsBean.fromJSON() and serializer.in(). Perhaps I'm confused - but is there a way to have similar API ? (there's a good likelihood I'm missing something here).

rodney said...

Small typo, there's a ")" in the URL pointing to Dispatch in the displayed README on http://github.com/debasishg/sjson/tree/master, it makes for a dead link. D'oh!

cheers!

Unknown said...

@Rodney : thanks for pointing this out .. fixed!