Scala - Instantiate classes dynamically from a Str

2019-09-06 15:46发布

问题:

I am trying to create a dynamic parser which allows me to parse json content into different classes depending on a class name.

I will get the json and the class name (as String) and I would like to do something like this:

val theCaseClassName = "com.ardlema.JDBCDataProviderProperties" 
val myCaseClass = Class.forName(theCaseClassName)
val jsonJdbcProperties = """{"url":"myUrl","userName":"theUser","password":"thePassword"}"""
val json = Json.parse(jsonJdbcProperties)
val value = Try(json.as[myClass])

The above code obviously does not compile because the json.as[] method tries to convert the node into a "T" (I have an implicit Reads[T] defined for my case class)

What would be the best way to get a proper "T" to pass in to the json.as[] method from the original String?

回答1:

A great solution that might work would be to do polymorphic deserialization. This allows you to add a field (like "type") to your json and allow Jackson (assuming you're using an awesome json parser like Jackson) to figure out the proper type on your behalf. It looks like you might not be using Jackson; I promise it's worth using.

This post gives a great introduction to polymorphic types. It covers many useful cases including the case where you can't modify 3rd party code (here you add a Mixin to annotate the type hierarchy).

The simplest case ends up looking like this (and all of this works great with Scala objects too -- jackson even has a great scala module):

object Test {
  @JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type"
  )
  @JsonSubTypes(Array(
    new Type(value = classOf[Cat], name = "cat"),
    new Type(value = classOf[Dog], name = "dog")
  ))
  trait Animal

  case class Dog(name: String, breed: String, leash_color: String) extends Animal
  case class Cat(name: String, favorite_toy: String) extends Animal

  def main(args: Array[String]): Unit = {
    val objectMapper = new ObjectMapper with ScalaObjectMapper
    objectMapper.registerModule(DefaultScalaModule)

    val dogStr = """{"type": "dog", "name": "Spike", "breed": "mutt",  "leash_color": "red"}"""
    val catStr = """{"type": "cat", "name": "Fluffy", "favorite_toy": "spider ring"}"""

    val animal1 = objectMapper.readValue[Animal](dogStr)
    val animal2 = objectMapper.readValue[Animal](catStr)

    println(animal1)
    println(animal2)
  }
}

This generates this output:

// Dog(Spike,mutt,red)
// Cat(Fluffy,spider ring)

You can also avoid listing the subtype mapping, but it requires that the json "type" field is a bit more complex. Experiment with it; you might like it. Define Animal like this:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.CLASS,
  include = JsonTypeInfo.As.PROPERTY,
  property = "type"
)
trait Animal

And it produces (and consumes) json like this:

/*
{
    "breed": "mutt",
    "leash_color": "red",
    "name": "Spike",
    "type": "classpath.to.Test$Dog"
}
{
    "favorite_toy": "spider ring",
    "name": "Fluffy",
    "type": "classpath.to.Test$Cat"
}
*/


回答2:

You should select your Reads[T] based on the class name. Unfortunately this will probably have to be a manual pattern match:

val r: Reads[_] = theCaseClassName match {
  case "com.ardlema.JDBCDataProviderProperties" => JDBCReads
  case ... => ...
}
val value = json.as(r).asInstanceOf[...]

Alternately, look at the implementation of json.as; at some point it's probably requiring a classTag and then calling .runtimeClass on it. Assuming that's so, you can just do whatever it is and pass your own myCaseClass there.