Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert polymorphic case classes to json and back

I am trying to use spray-json in scala to recognize the choice between Ec2Provider and OpenstackProvider when converting to Json and back. I would like to be able to give choices in "Provider", and if those choices don't fit the ones available then it should not validate.

My attempt at this can be seen in the following code:

import spray.json._
import DefaultJsonProtocol._ 

case class Credentials(username: String, password: String)
abstract class Provider
case class Ec2Provider(endpoint: String,credentials: Credentials) extends Provider
case class OpenstackProvider(credentials: Credentials) extends Provider
case class Infrastructure(name: String, provider: Provider, availableInstanceTypes: List[String])
case class InfrastructuresList(infrastructures: List[Infrastructure])

object Infrastructures extends App with DefaultJsonProtocol {
   implicit val credFormat = jsonFormat2(Credentials)
   implicit val ec2Provider = jsonFormat2(Ec2Provider)
   implicit val novaProvider = jsonFormat1(OpenstackProvider)
   implicit val infraFormat = jsonFormat3(Infrastructure)
   implicit val infrasFormat = jsonFormat1(InfrastructuresList)

  println(
    InfrastructuresList(
      List(
        Infrastructure("test", Ec2Provider("nova", Credentials("user","pass")), List("1", "2")) 
      )
    ).toJson
  )
}

Unfortunately, it fails because it can not find a formatter for Provider abstract class.

test.scala:19: could not find implicit value for evidence parameter of type Infrastructures.JF[Provider]

Anyone have any solution for this?

like image 512
wernerb Avatar asked May 12 '13 21:05

wernerb


1 Answers

What you want to do is not available out of the box (i.e. via something like type hints that allow the deserializer to know what concrete class to instantiate), but it's certainly possible with a little leg work. First, the example, using a simplified version of the code you posted above:

case class Credentials(user:String, password:String)
abstract class Provider
case class Ec2Provider(endpoint:String, creds:Credentials) extends Provider
case class OpenstackProvider(creds:Credentials) extends Provider
case class Infrastructure(name:String, provider:Provider)

object MyJsonProtocol extends DefaultJsonProtocol{
  implicit object ProviderJsonFormat extends RootJsonFormat[Provider]{
    def write(p:Provider) = p match{
      case ec2:Ec2Provider => ec2.toJson
      case os:OpenstackProvider => os.toJson
    }

    def read(value:JsValue) = value match{
      case obj:JsObject if (obj.fields.size == 2) => value.convertTo[Ec2Provider]
      case obj:JsObject => value.convertTo[OpenstackProvider]
    }
  }

  implicit val credFmt = jsonFormat2(Credentials)
  implicit val ec2Fmt = jsonFormat2(Ec2Provider)
  implicit val openStackFmt = jsonFormat1(OpenstackProvider)
  implicit val infraFmt = jsonFormat2(Infrastructure)
}

object PolyTest {
  import MyJsonProtocol._

  def main(args: Array[String]) {
    val infra = List(
      Infrastructure("ec2", Ec2Provider("foo", Credentials("me", "pass"))),
      Infrastructure("openstack", OpenstackProvider(Credentials("me2", "pass2")))
    )
    val json = infra.toJson.toString
    val infra2 = JsonParser(json).convertTo[List[Infrastructure]]
    println(infra == infra2)
  }
}

In order to be able to serialize/deserialize instances of the abstract class Provider, I've created a custom formatter where I am supplying operations for reading and writing Provider instances. All I'm doing in these functions though is checking a simple condition (binary here as there are only 2 impls of Provider) to see what type it is and then delegating to logic to handle that type.

For writing, I just need to know which instance type it is which is easy. Reading is a little trickier though. For reading, I'm checking how many properties the object has and since the two impls have different numbers of props, I can differentiate which is which this way. The check I'm making here is very rudimentary, but it shows the point that if you can look at the Json AST and differentiate the types, you can then pick which one to deserialize to. Your actual check can be as simple or as complicated as you like, as long as it's is deterministic in differentiating the types.

like image 139
cmbaxter Avatar answered Oct 14 '22 05:10

cmbaxter