DNA
DNA

Reputation: 42586

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 Maps 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.

Upvotes: 0

Views: 79

Answers (2)

Alexey Romanov
Alexey Romanov

Reputation: 170713

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.

Upvotes: 1

Edmondo
Edmondo

Reputation: 20080

You probably want to use a JSON library for the purpose, I personally suggest Circe. It handles smoothly typesafe serialization and deserialization and can be extended easily. If you are looking for polymorphic deserialization though, you will need to store the type inside the JSON and Circe typically handles these too, via support of sealed hierarchy of case classes.

Upvotes: 0

Related Questions