4

I want to pass a type to a function in Scala.

Problem in detail

First iteration

I have the following Java classes (coming from an external source):

public class MyComplexType {
    public String name;
    public int number;
}

and

public class MyGeneric<T> {
    public String myName;
    public T myValue;
}

In this example I want MyComplexType to be the the actual type of MyGeneric; in the real problem there are several possibilities.

I want to deserialize a JSON string using a Scala code as follows:

import org.codehaus.jackson.map.ObjectMapper

object GenericExample {
  def main(args: Array[String]) {
    val jsonString = "{\"myName\":\"myNumber\",\"myValue\":{\"name\":\"fifteen\",\"number\":\"15\"}}"
    val objectMapper = new ObjectMapper()
    val myGeneric: MyGeneric[MyComplexType] = objectMapper.readValue(jsonString, classOf[MyGeneric[MyComplexType]])
    val myComplexType: MyComplexType = myGeneric.myValue
  }
}

it compiles fine but runtime error occurs:

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to MyComplexType
        at GenericExample$.main(GenericExample.scala:9)

Second iteration

Working solution to the problem:

val jsonString = "{\"myName\":\"myNumber\",\"myValue\":{\"name\":\"fifteen\",\"number\":\"15\"}}"
val objectMapper = new ObjectMapper()
val myGeneric: MyGeneric[MyComplexType] = objectMapper.readValue(jsonString, classOf[MyGeneric[MyComplexType]]) 
myGeneric.myValue = objectMapper.readValue(objectMapper.readTree(jsonString).get("myValue").toString, classOf[MyComplexType])
val myComplexType: MyComplexType = myGeneric.myValue

Not nice but works. (If anybody knows how to make it better, that would also welcome.)

Third iteration

The lines in the solution of second iteration occur in the real problem several times, therefore I want to create a function. The altering variables are the JSON formatted string and the MyComplexType.

I want something like this:

def main(args: Array[String]) {
  val jsonString = "{\"myName\":\"myNumber\",\"myValue\":{\"name\":\"fifteen\",\"number\":\"15\"}}"
  val myGeneric = extractMyGeneric[MyComplexType](jsonString)
  val myComplexType: MyComplexType = myGeneric.myValue
}

private def extractMyGeneric[T](jsonString: String) = {
  val objectMapper = new ObjectMapper()
  val myGeneric = objectMapper.readValue(jsonString, classOf[MyGeneric[T]])    
  myGeneric.myValue = objectMapper.readValue(objectMapper.readTree(jsonString).get("myValue").toString, classOf[T])
  myGeneric
}

This does not work (compiler error). I've already played around with various combinations of Class, ClassTag, classOf but none of them helped. There were compiler and runtime errors as well. Do you know how to pass and how to use such a type in Scala? Thank you!

Csaba Faragó
  • 443
  • 1
  • 4
  • 14

2 Answers2

1

When you use jackson to parse json, you can use TypeReference to parse generic type. Example:

val jsonString = "{\"myName\":\"myNumber\",\"myValue\":{\"name\":\"fifteen\",\"number\":\"15\"}}"
val objectMapper = new ObjectMapper()
val reference = new TypeReference[MyGeneric[MyComplexType]]() {}
val value: MyGeneric[MyComplexType] = objectMapper.readValue(jsonString, reference) 

if you still want to use Jackson, I think you can create a parameter with TypeReference type. like:

  implicit val typeReference = new TypeReference[MyGeneric[MyComplexType]] {}
  val value = foo(jsonString)
  println(value.myValue.name)


  def foo[T](jsonStr: String)(implicit typeReference: TypeReference[MyGeneric[T]]): MyGeneric[T] = {
    val objectMapper = new ObjectMapper()
    objectMapper.readValue(jsonStr, typeReference)
  }
chengpohi
  • 14,064
  • 1
  • 24
  • 42
  • Thank you for the answer! To extend it: the `TypeReference`should be imported as follows: `import org.codehaus.jackson.\`type\`.TypeReference`. – Csaba Faragó Apr 15 '16 at 14:33
  • This is a nice solution for "first iteration" problem, thank you! However, it does not solve the problem raised in the "third iteration", i.e. the main problem. That is still open; indeed there are a few more other things to do, therefore it would be nice to create a separate function. The most straightforward solution (passing `MyGeneric[MyComplexType]` as generic type) results in runtime error `java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to MyGeneric`. – Csaba Faragó Apr 15 '16 at 14:47
  • @CsabaFaragó you can check my update, hope it can help you. – chengpohi Apr 15 '16 at 16:37
  • "if you still want to use `Jackson`" - indeed, what else can I use? – Csaba Faragó Apr 17 '16 at 08:53
  • Nice list, thank you! Jackson seemed to be the most common approach in Scala. Which one is currently the most preferred main stream solution? – Csaba Faragó Apr 18 '16 at 04:53
1

Using your approach, I think this is how you can get classes that you need using ClassTags:

def extractMyGeneric[A : ClassTag](jsonString: String)(implicit generic: ClassTag[MyGeneric[A]]): MyGeneric[A] = {
  val classOfA = implicitly[ClassTag[A]].runtimeClass.asInstanceOf[Class[A]]
  val classOfMyGenericOfA = generic.runtimeClass.asInstanceOf[Class[MyGeneric[A]]]
  val objectMapper = new ObjectMapper()
  val myGeneric = objectMapper.readValue(jsonString, classOfMyGenericOfA)
  myGeneric.myValue = objectMapper.readValue(objectMapper.readTree(jsonString).get("myValue").toString, classOfA)
  myGeneric
}

I am not familiar with jackson but in play-json you could easily define Reads for your generic class like this

import play.api.libs.functional.syntax._
import play.api.libs.json._

implicit def genReads[A: Reads]: Reads[MyGeneric[A]] = (
  (__ \ "myName").read[String] and
  (__ \ "myValue").read[A]
)((name, value) => {
    val e = new MyGeneric[A]
    e.myName = name
    e.myValue = value
    e
})

Having this, and provided that instance of Reads for MyComplexType exists, you can implement your method as

def extractMyGeneric[A: Reads](jsonString: String): MyGeneric[A] = {
  Json.parse(jsonString).as[MyGeneric[A]]
}

the issue here is that you need to provide Reads for all of your complex types, which would be as easy as

implicit complexReads: Reads[MyComplexType] = Json.reads[MyComplexType]

if those were case classes, otherways I think you would have to define them manually in simillar way to what I've done with genReads.

Łukasz
  • 8,555
  • 2
  • 28
  • 51
  • Thank you for your detailed answer! Meanwhile the other thread solved my problem, along with a Jackson improvement as well. I save your solution for possible further type related problems. – Csaba Faragó Apr 16 '16 at 07:10