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 promotes JSON4s 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
case class Person (id: Int,name: String,age: Int,addressId:Int)
case class Address (id: Int, street: String,city: String)
// no direct reference, to fit with slick database models
case class PersonWithAddress(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 json
val 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._
object ImplicitConversion {
// data model based on http://slick.typesafe.com/doc/2.1.0/orm-to-slick.html
case class Person (id: Int,name: String,age: Int,addressId:Int)
case class Address (id: Int, street: String,city: String)
// no direct reference, to fit with slick database models
case class PersonWithAddress(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 sample
val 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 json
val json = pa.asJson
var content = json.toString()
println (content)
// we should get a person instance here
var 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