0

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

2 Answers 2

3

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"
}
*/
Sign up to request clarification or add additional context in comments.

5 Comments

That sounds really cool but I am using Play framework which contains a very powerful JSON library so I wouldn't like to include another dependency to an external library. Not sure if I can't do something like that with the Play json library though.
*cough *cough; clearly not all that powerful if it doesn't give you polymorphic deserialization. Looking at their API it doesn't seem possible to get an instance of Reads[T] from a classOf[T] or from a type token. I think the only option is the one that @lmm suggests if you're unwilling to use Jackson.
I've just given a try at your code and getting the following exception: Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.stratio.datavis.Test$Animal, problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information at [Source: {"type": "dog", "name": "Spike", "breed": "mutt", "leash_color": "red"}; line: 1, column: 1] at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:164)
@ardlema It sounds like you might be missing the @JsonSubTypes Mapping on the Animal trait. Which Animal trait are you using? I just copy/pasted the top block into a current project imported: com.fasterxml.jackson.annotation._ and com.fasterxml.jackson.annotation.JsonSubTypes.Type and it works just fine. I'm using Jackson 2.4.3.
Cool! Yes, it was my fault, I was not importing the proper classes. It is working now. I think I am going down this route. Thank you very much for your help!!
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.

1 Comment

Sounds good but I would like to avoid implementing a pattern matching with all the options (it will break my "dynamic" idea). I will have a look at the json.as implementation as you suggested.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.