Quantcast
Viewing all articles
Browse latest Browse all 33

Implicit JSON conversion with Scala

In my last blog posts about Scala, I explained Scalatra with Bearer Authentication and Asynchronous HTTP requests with Scala and Dispatch. Today I am going to focus on JSON. We will encode data types to JSON and decode JSON into existing data structures.

JSON Library

Plenty of options are available for JSON in Scala. It really depends on your setup and may depend on your web framework decision. I am not going to start an argumentation about the best toolkit available. The following post will stick with Argonaut, but the general approach works with other JSON frameworks as well. I selected it because it fits my function development approach in Scala and works well with Scalatra. Be aware that Scalatra itself promotesJSON4s with Jackson, but I found it more difficult to use that Argonaut.

Conversion

Before we talk about the conversion, we need to define the data model. I use the slick sample structuree

caseclassPerson(id: Int,name: String,age: Int,addressId:Int)caseclassAddress(id: Int, street: String,city: String)// no direct reference, to fit with slick database modelscaseclassPersonWithAddress(person: Person, address: Address)

Now we are able to store a person with address:

  val person = Person(0, "John Rambo" , 67, 0)
  val address = Address(0, "101 W Main St", "Madison, Kentucky")
  val pa = PersonWithAddress(person, address)

If you are developing REST services you may use JSON as input and output data format. It would be a horror if we have to manually construct the conversion for each route. Therefore we want something like:

// convert the person to jsonval json = pa.asJson

Scala provides a cool feature for implicit type conversion. This is very handy for our use case and fits with argonaut codec. For our case, the following definition is appropriate:

// implicit conversion with argonaut
  implicit def PersonAddressEncodeJson: EncodeJson[PersonWithAddress] =
  EncodeJson((p: PersonWithAddress) =>
    ("id" := p.person.id) ->:
    ("name" := p.person.name) ->:
    ("age" := p.person.age) ->:
    ("address" := Json (
      ("id" := p.address.id),
      ("street" := p.address.street),
      ("city" := p.address.city)
    )
  ) ->: jEmptyObject)

Now we have all pieces together to convert Scala data types to JSON. The result looks as follows:

{
  "id": 0,
  "name": "John Rambo",
  "age": 67,
  "address": {
    "id": 0,
    "street": "101 W Main St",
    "city": "Madison, Kentucky"}
}

To consume JSON data we write a decoder:

implicit def PersonAddressDecodeJson: DecodeJson[PersonWithAddress] =
  DecodeJson(c => for {

    id <- (c --\ "id").as[Int]
    name <- (c --\ "name").as[String]
    age <- (c --\ "age").as[Int]
    address <- (c --\ "address").as[Json]

    // extract data from address
    addressid <- (address.acursor --\ "id").as[Int]
    street <- (address.acursor --\ "street").as[String]
    city <- (address.acursor --\ "city").as[String]

  } yield PersonWithAddress(Person(id, name, age, addressid), Address(addressid, street, city)))

As a result we get a valid Scala object with the expected data

PersonWithAddress(Person(0,John Rambo,67,0),Address(0,101 W Main St,Madison, Kentucky))

Compared to Nodejs, this decode is also a type validation. The conversion will fail, if one parameter is missing. In case you would like to make parts optional, you need to adapt the case class and use Option[Int] instead of Int. Additionally you need to adapt the decode and parse to Option[Int]:

  age <- (c --\ "age").as[Option[Int]]

The complete sample:

import argonaut._, Argonaut._

objectImplicitConversion {// data model based on http://slick.typesafe.com/doc/2.1.0/orm-to-slick.htmlcaseclassPerson(id: Int,name: String,age: Int,addressId:Int)caseclassAddress(id: Int, street: String,city: String)// no direct reference, to fit with slick database modelscaseclassPersonWithAddress(person: Person, address: Address)// implicit conversion with argonaut
  implicit def PersonAddressEncodeJson: EncodeJson[PersonWithAddress] =
  EncodeJson((p: PersonWithAddress) =>
    ("id" := p.person.id) ->:
    ("name" := p.person.name) ->:
    ("age" := p.person.age) ->:
    ("address" := Json (
      ("id" := p.address.id),
      ("street" := p.address.street),
      ("city" := p.address.city)
    )
  ) ->: jEmptyObject)

  implicit def PersonAddressDecodeJson: DecodeJson[PersonWithAddress] =
  DecodeJson(c => for {

    id <- (c --\ "id").as[Int]
    name <- (c --\ "name").as[String]
    age <- (c --\ "age").as[Int]
    address <- (c --\ "address").as[Json]

    // extract data from address
    addressid <- (address.acursor --\ "id").as[Int]
    street <- (address.acursor --\ "street").as[String]
    city <- (address.acursor --\ "city").as[String]

  } yield PersonWithAddress(Person(id, name, age, addressid), Address(addressid, street, city)))

  def main(args: Array[String]) {
    // running a sampleval person = Person(0, "John Rambo" , 67, 0)
    val address = Address(0, "101 W Main St", "Madison, Kentucky")
    val pa = PersonWithAddress(person, address)

    // convert the person to jsonval json = pa.asJson
    var content = json.toString()

    println (content)

    // we should get a person instance herevar padecoded : PersonWithAddress = content.decodeOption[PersonWithAddress].get

    println (padecoded)
  }
}

Conclusion

With a few lines of code, we encapsulate the complete JSON encoding and decoding. Its amazing how easy it is. It may not be as intuitive as in Nodejs, but comes with additional type checks, which is generally more complex in Nodejs.

If you have any questions contact me via Twitter @chri_hartmann or Github


Viewing all articles
Browse latest Browse all 33

Trending Articles