DNA DNA - 26 days ago 10
Scala Question

Handling mixed values of type Any without asInstanceOf boilerplate

This issue occurs for any API that can return multiple classes, but in a collection of type

Any
.

A specific example is handling JSON using the built-in JSON parser (
scala.util.parsing.json
): the value returned is a
Map[String,Any]
because the value in each JSON key-value pair can be any JSON type.

Extracting values from these nested
Map
s seems to require type testing and casting, which is rather ugly. In particular, we end up with multiple functions that are identical apart from the return type (e.g. String, Double, Map...), which is used for checking and casting.

Is it possible to abstract out this type so that only one generic
get[T](...): T
function is required, avoiding this boilerplate?

I have been looking at
TypeTag
but all the examples I've found so far look at abstracting over the argument type, not the return type.

To clarify: I'm aware that there are many other JSON parsers that provide much nicer interfaces with pattern matching etc, but I'm just interested in this general problem of refactoring, for dealing with legacy interfaces that return collections of
Any
.

import scala.util.parsing.json._

object ParseJSON {

val text = """{"str":"A String", "num":123, "obj": { "inner":"value" }}"""

val json = JSON.parseFull(text).get.asInstanceOf[Map[String,Any]]
//> ... Map(str -> A String, num -> 123.0, obj -> Map(inner -> value))

// Three essentially identical functions:

def getString(m:Map[String,Any], k:String): Option[String] = {
m.get(k).flatMap{ v =>
if (v.isInstanceOf[String]) Some(v.asInstanceOf[String]) else None
}
}

def getDouble(m:Map[String,Any], k:String): Option[Double] = {
m.get(k).flatMap{ v =>
if (v.isInstanceOf[Double]) Some(v.asInstanceOf[Double]) else None
}
}

def getObject(m:Map[String,Any], k:String): Option[Map[String, Any]] = {
m.get(k).flatMap{ v =>
if (v.isInstanceOf[Map[_,_]]) Some(v.asInstanceOf[Map[String,Any]])
else None
}
}

getString(json, "str") //> res0: Option[String] = Some(A String)
getString(json, "num") //> res1: Option[String] = None
getObject(json, "obj")
//> res3: Option[Map[String,Any]] = Some(Map(inner -> value))
}


I initially thought this could be solved via a generic class:

class Getter[T] {
def get(m: Map[String, Any], k: String): Option[T] = {
m.get(k).flatMap { v =>
if (v.isInstanceOf[T]) Some(v.asInstanceOf[T]) else None
}
}
}

new Getter[String].get(json, "str")


but as Oleg Pyzhcov pointed out (in my now-deleted answer), type erasure prevents this from detecting whether the types are correct at runtime.

Answer Source

The fix to your failed attempt is quite simple:

import scala.reflect.ClassTag

class Getter[T: ClassTag] {
  def get(m: Map[String, Any], k: String): Option[T] = {
    m.get(k).flatMap {
      case v: T => Some(v) 
      case _ => None
    }
  }
}

new Getter[String].get(json, "str")

Pattern-matching against : T is handled specially when a ClassTag[T] is available.

Unfortunately, if you want T itself to be a generic type, type erasure strikes back: Getter[List[String]] can only check if it's passed a List, not its type parameter.